概念:
进程:在操作系统中可以并发执行的一个任务,采用分时间片(微观串行,宏观并行),由操作系统调度
线程: 是进程中并发执行的一个顺序流程
线程组成:
CPU时间片,由操作系统调度
内存(JVM内存):堆空间(保存对象,即实例变量,线程共享)、栈空间(保存局部变量:线程独立)
代码:是由程序员决定
多线程:是指从软件或者硬件上实现多个线程并发执行的技术
状态:就绪,执行,synchronized阻塞,wait和sleep挂起,结束
调用线程start方法后线程进入就绪状态,线程调度系统将线程就绪状态转为运行状态,遇到synchronized方法,由运行状态转为阻塞状态,synchronized获得锁后,由阻塞转到运行状态,在这种情况下可以调用wait方法转为挂起状态,当线程关联的代码执行完毕后。线程变为结束状态
图片: https://uploader.shimo.im/f/zbN9uuTSgz8Xd02C.png
多线程实现方式:
继承Thread类创建线程
实现Runnable接口创建线程
实现Runnable接口可能更优.原因有二:
Java不支持多继承。因此扩展Thread类就代表这个子类不能扩展其他类。而实现Runnable接口的类还可能扩展另一个类。
类可能只要求可执行即可,因此继承整个Thread类的开销过大。
线程调用start启动后自动执行run方法,run方法是自定义代码
使用继承Thread的类(推荐使用匿名内部类)
class MyThread extends Thread{public void run(){ 该线程的自定义流程;}}
main: Thread t1 = new MyThread();
t1.start(); //线程启动
t1.run(); //线程并未启动,只是最普通的函数调用
使用实现Runnable接口的类(推荐使用匿名内部类),缺点是不能抛异常,无返回值;可参考下文Callable接口
class MyThread implements Runnable{public void run() { 该线程的自定义流程}}
main:Runnable rthread = new MyThread();
Thread t2 = new Thread(rthread);
t2.start();
//匿名内部类:
Thread t = new Thread(new Runnable() {
public void run() {}
});
}
线程同步
线程同步:即当有一个线程在对内存进行操作时,其他线程都不可以对这个内存地址进行操作,直到该线程完成操作, 其他线程才能对该内存地址进行操作,其他线程又处于等待状态
为什么要线程同步:在多线程环境下,并发访问同一个对象(临界资源),由于彼此间切割此对象的原子性操作,对象的状态就会处于一个不一致的状态。
在java中,任意对象(Object o)都有一把互斥锁标记(syjchronized(o))用于分配线程
①.synchronized:(同步)互斥锁
第一种:同步代码块
Object o = new Object();
synchronized(o) { 需要同步的代码块1}
synchronized(o) { 需要同步的代码块2}
总结:当一个线程要执行同步代码块时,必须获得锁标记对象的互斥锁标记,没有获取到时就进入线程阻塞,知道得到互斥锁标记。线程执行同步代码块,执行完毕后自动释放互斥锁标记,归还给锁对象。
线程是否同步,取决于是否争用同一锁对象
第二种:同步方法
public synchronized void method() {}
总结:当线程要执行同步实例方法时,必须获得当前对象的互斥锁标记,不能获得就进入阻塞。当获取互斥锁标记执行完同步方法,自动释放互斥锁标记,并归还给对象。
一个线程可以在持有互斥锁标记的前提下获取其他锁对象的互斥锁标记
②.Lock:锁接口,提供了比synchronized更广泛的所定义语句(有个实现类为ReentrantLock)
ReadWriteLock:读写锁,持有一对读锁和写锁,(有一个实现类ReenTrantReadWriteLock)
读锁:允许分配给多个线程(并发) readLock();
写锁:最多只允许分配给一个线程(互斥), writeLock(); 读锁和写锁两者互斥
总结:当线程持有读锁时,不能分配写锁给其他线程,但可以分配读锁;同样在线程持有写锁时,也不能分配读锁和写锁给其他线程。(读读并发、写写互斥、读写互斥)
用法:ReadWriteLock rwl = new ReentrantReadWriteLock();
Lock readLock = rwl.readLock(); //读锁
Lock writeLock = rwl.writeLock(); //写锁
readLock.lock();
中间为获得读锁的代码块,如果有try/catch时,可将readLock.unlock();写在finally代码块里
readLock.unlock();
writeLock用法与此相同
③.join()方法
如有线程t1,如果有线程t2要在t1执行完再执行时,则可在t2的代码块中添加t1.join();语句
例如:在main函数中有线程t1、t2、t3,当线程执行完在执行输出语句时:
public static void main (String[] args) {
t1.join(); //将三个线程添加到主线程,并且先执行这三个线程
t2.join();
t3.join();
System.out.println(“输出语句”);
}
④.线程池ExecutorService es = Executors.newFixedThreadPool(int n);//创建线程池,并设置固定并发线程的个数n,
将实现Callable接口(Callable
再用Future
用法:
Callable
public Integer call() { return null;} //需要实现call方法
};
ExecutorService es = Executors.newFixedThreadPool(2);//可并发两个线程
Future
Future
System.out.println(f1.get());//将执行的结果通过future对象的get()方法获取
⑤.fork-join框架(重要,自从jdk1.7),也是线程池
思想:分治-归并算法,大任务--->小任务--->将小任务的结果合并成完整结果
用法:
class Task extends RecursiveTask
protected T compute() {
return T类型数据或null;
}
}
main: ForkJoinPool fjp = new ForkJoinPool();
Task t1 = new Task();
fjp.invoke(new Task()); //将线程放入线程池
与此同时可以在执行再一个当前的线程t2
Task t2 = new Task();
T tmp2 = t2.compute(); //将线程t2执行后的结果返回给tmp2
T tmp1 = t1.join(); //在线程t2中将t1执行后的结果返回给tmp1
实现线程安全的方法
1).加锁:synchronized,互斥锁,对对象加锁,缺点是并发效率低
2).锁分级:ReadWriteLock,分配写锁时,不分配读锁;写锁未分配时,可分配多个读锁
3).锁分段:ConcurrentHashMap,对整个Map加锁,分16个片段,分别独自加锁(jdk5 – jdk7)
4).无锁算法:CAS比较交换算法(利用状态值)
ConcurrentHashMap,利用CAS算法,自从jdk8
详解:例如对一个变量n=3进行多线程访问
执行下面代码:
where(true) {
int status = n; //旧值
if(status == n) { //拿旧值和当前值再次做比较
//该线程可对n进行修改操作;假如在判断之前n的值被其他线程修改了,则从新这个 循环步骤
}
}
5).尽量回避临界资源,尽可能的使用局部变量
线程间的通信(等待 -- 通知 机制)
例如:有两个t1,t2线程,有对象Object o = new Object();
t1: 如果使用o.wait();必须出现在对o加锁的同步代码块中,此时,t1将会释放它拥有的所有锁标记,并进入o的等待队列(阻塞)和释放CPU资源
synchronized(o) { o.wait(); }
t2: 此时使用t1所释放的锁标记,利用o.notify() / o.notifyAll(),此方法也有放在对o加锁的同步代码块中,
t2会从o的等待队列中释放一个或全部线程
wait 和 sleep方法的区别是:wait会失去锁标记,而sleep不会失去锁标记
常见面试题
-
当一个线程进入一个对象的一 个synchronized 方法后,其它线程是否可进入此对象的其它方法?
-
其他方法前是否加了synchonized的关键字,如果没加则就能
-
如果这个方法内部加了wait方法,则可以进入其他的synchorized方法
-
如果其他方法内部加了synchorized方法,并且没有wait方法,则不能。
-
如果其他方法是static,它用的同步锁就是当前类的字节码,与非静态的方法不能同步,因为非静态方法用的是this.
说说进程,线程,协程之间的区别
进程是程序运行和资源分配的基本单位,一个程序至少有一个进程,一个进程至少有一个线程。
进程在执行过程中拥有独立的内存单元,而多个线程共享内存资源,减少切换次数,从而效率更高。
线程是是cpu调度和分派的基本单位。同一进程中的多个线程之间可以并发执行。
你了解守护线程吗?它和非守护线程有什么区别
程序运行完毕,jvm会等待非守护线程完成后关闭,但是jvm不会等待守护线程。守护线程最典型的例子就是GC线程。
什么是多线程上下文切换
多线程的上下文切换是指CPU控制权由一个已经正在运行的线程切换到另外一个就绪并等待获取CPU执行权的线程的过程。