想了想还是从基础开始整理并发编程的知识吧,实在是太多了!!!
并发编程基础
-
基础概念:
-
线程与进程
- 进程:进程是系统进行
资源分配和调度
的基本单位,平时我们在电脑上启动的一个程序就是一个进程。 - 线程:线程是
操作系统
进行调度
的最小单位 - 关系:一个进程可以启动一个或多个线程,进程中所有的线程会
共享
进程中的内存空间
,每个线程都有自己的程序计数器
和栈区域
,进程中的栈资源用来存储该线程的局部变量
,这些局部变量是线程私有
的 - 线程与进程的关系如图所示:
- 进程:进程是系统进行
-
线程的创建
-
Java中有三种线程创建方式,分别是实现
Rannable接口
的run方法、继承Thread类
重写run方法、实现Callable接口
-
继承Thread
继承Thread类的实现代码如下:/** * @author admin */ public class ThreadTest { //继承Thread类并重写run方法 public static class MyThread extends Thread{ @Override public void run(){ System.out.println("a thread"); } } public static void main(String[] args) { //创建线程 MyThread myThread = new MyThread(); //启动线程 myThread.start(); } }
-
在以上代码中MyThread继承Thread类,并重写了run方法。在main函数中创建MyThread实例,然后调用MyThread的
start()
方法启动该线程。但是我们要知道在创建MyThread
对象后线程并没有立即启动执行,需要调用start方法后才启动线程。 -
调用
start
方法之后线程并没有马上执行而是线程处于就绪状态
,等待获取到CPU资源之后才真正处于运行状态
。 -
run方法执行完毕,线程就处于
终止状态
。 -
使用继承Thread类的好处是,在
run
方法内获取线程时直接调用this
就可以了,无需通过Thread.currentThread()
方法;不好的地方是Java不允许多继承
,如果继承了Thread类就无法继承其它类。并且任务与代码没有分离,当多个线程执行一样的任务时需要多份任务代码。
-
-
实现Rannable
实现Runnable接口代码如下:
public static class RunnableTask implements Runnable{ @Override public void run() { System.out.println("a runnable thread"); } } public static void main(String[] args) { RunnableTask runnableTask = new RunnableTask(); new Thread(runnableTask).start(); new Thread(runnableTask).start(); }
- 在以上代码中,两个线程公用了一个runnableTask代码逻辑,并且也可以根据需要给RunnableTask
添加参数
进行区分(重载
)。另外RunnableTask可以继承其它类。但是继承Thread类和实现Runnable接口的方式都是没有返回值
的。
- 在以上代码中,两个线程公用了一个runnableTask代码逻辑,并且也可以根据需要给RunnableTask
-
实现Callable接口
实现Callable接口方式://创建任务类 public static class CallTask implements Callable<String>{ @Override public String call() throws Exception { return "hello callAble"; } } public static void main(String[] args) throws InterruptedException { //创建异步任务 FutureTask<String> futureTask = new FutureTask<>(new CallTask()); //启动线程 new Thread(futureTask).start(); try { //等待任务执行结束并返回结果 String result = futureTask.get(); System.out.println(result); }catch (ExecutionException e){ e.printStackTrace(); } }
- 在以上代码中实现
Callable
接口的call()
方法,在main函数中创建一个FutureTask
对象(构造函数为CallTask的实例),然后使用创建的FutureTask对象作为任务创建一个线程并启动它,最后通过futureTask.get()
等待任务执行完毕并返回结果。
- 在以上代码中实现
-
总结:
- 使用继承Thread类的好处是方便传参数,可以在子类中添加成员变量,通过set方法设置参数或者构造函数进行传递;如果使用实现Runnable接口的方式,则只能使用主线程里面被声明为final的变量,但是前两种都没有返回结果,但是Callable接口可以实现获取返回结果。
-
-
线程等待和通知
-
Object级别
-
wait
:当一个线程调用共享变量的wait()方法时,该调用线程会被阻塞挂起
。-
直到发生一下几件事才返回:(1)其它线程调用该共享对象的notify()或者notifyAll()方法,(2)其它线程调用了该线程的interrupt()方法,该线程抛出InterruptedException异常返回。
-
另外,如果调用wait()方法的线程没有
事先获取对象的监视器锁
,则调用wait()方法时调用线程会抛出IllegalMonitorStateException异常。一个线程获取该共享变量的监视器锁的方式有两种:1、执行synchronized同步代码块,使用该共享变量作为参数synchronized(共享变量){ //doSomething }
2、调用该共享变量的方法,并且该方法使用了synchronized修饰
synchronized void add(int a,int b){ //doSomething }
-
另外需要注意的是,一个线程可以从挂起状态变为可以运行状态,即使该线程没有被其它线程调用notify()、notifyAll()方法进行唤醒,或者被中断等,这就是所谓的虚假唤醒。虽然虚假唤醒很少发生,但是要防患于未然,做法就是不停的测试该线程被唤醒的条件是否满足,不满足就继续等待,也就是说在一个循环调用中调用wait()方法进行防范。退出的条件是满足了唤醒该线程的条件。
synchronized(obj){ while(条件不满足){ obj.wait(); } }
-
线程挂起的方法也存在一个wait(long timeout)的方法,如果一个线程调用共享对象的wait(long timeout)方法后,如果没有在指定的timeout ms时间内被其它线程调用该共享变量的notify()和notifyAll()方法唤醒,那么该方法会因为超时而返回。如果将timeout设置为0,则效果和wait()方法效果一样。
-
-
notify
- 一个线程调用共享对象的notify()方法后,会唤醒一个在共享变量上调用wait系列方法后被挂起的线程。一个共享变量上可能有多个线程在等待,具体唤醒哪个线程是随机的。
- 此外被唤醒的线程并不会马上从wait方法返回并执行,它必须在
获取了共享对象的监视器锁后才可以返回
,也就是唤醒它的线程释放了共享变量的监视器锁之后,被唤醒的线程也不一定会直接获取到共享变量的监视器锁,因为该线程还需要和其它线程竞争该锁(所以说synchronized锁并不是公平的
),只有竞争到共享变量的监视器锁(synchronized锁)之后才可以继续执行。
-
notifyAll
-
不同于notify()会唤醒被阻塞到该共享变量的一个线程,notifytAll()会
唤醒所有
的该共享变量上调用wait
系列方法被挂起的线程。//创建共享变量 private static volatile Object object = new Object(); public static void main(String[] args) throws InterruptedException { //创建线程A Thread threadA = new Thread(new Runnable() { @Override public void run() { synchronized (object) { try { System.out.println("threadA start wait"); object.wait(); System.out.println("threadA wait end"); } catch (InterruptedException e) { e.printStackTrace(); } } } }); //创建线程B Thread threadB = new Thread(new Runnable() { @Override public void run() { synchronized (object) { try { System.out.println("threadB start wait"); object.wait(); System.out.println("threadB wait end"); } catch (InterruptedException e) { e.printStackTrace(); } } } }); //创建线程C Thread threadC = new Thread(new Runnable() { @Override public void run() { synchronized (object) { System.out.println("threadC start notifyAll"); object.notifyAll(); System.out.println("threadC notifyAll end"); } } }); threadA.start(); threadB.start(); Thread.sleep(1000); threadC.start(); //等待线程结束 threadA.join(); threadB.join(); threadC.join(); }
如上代码中我们创建两个线程并都执行wait()方法挂起线程,然后沉睡一秒之后执行线程C执行notifyAll(),这样线程A和线程B被同时唤醒,因此线程A和线程B需要竞争共享变量object的监视器锁,谁先获取到监视器锁谁就先执行。如上的执行结果中我们可以看到线程A先获取到了监视器锁然后执行了后续的操作。
-
-
-
Thread级别
-
sleep
:让线程睡眠的方法
-
当一个执行中的线程执行了sleep()方法后,该线程会暂时
让出CPU的执行权,不参与CPU的调度
,但是该线程仍然持有监视器锁(也就是不会释放锁)
。当指定的睡眠时间到了之后该函数会正常返回,线程就处于就绪状态
,重新参与CPU调度,获取到CPU资源之后就可以继续运行。如果在睡眠期间其它线程调用了睡眠中的线程的interrupt()方法中断该线程,那么该线程会在调用sleep()的地方抛出IntermptedException异常并返回。public class SleepTest { //创建一个独占锁 private static final Lock lock = new ReentrantLock(); public static void main(String[] args) throws InterruptedException { //创建线程A Thread threadA = new Thread(new Runnable() { @Override public void run() { //获取独占锁 lock.lock(); try { System.out.println("threadA start sleep"); Thread.sleep(10000); System.out.println("threadA end sleep"); }catch (InterruptedException e){ e.printStackTrace(); }finally { //释放锁 lock.unlock(); } } }); //创建线程B Thread threadB = new Thread(new Runnable() { @Override public void run() { //获取独占锁 lock.lock(); try { System.out.println("threadB start sleep"); Thread.sleep(10000); System.out.println("threadB end sleep"); }catch (InterruptedException e){ e.printStackTrace(); }finally { //释放锁 lock.unlock(); } } }); //启动线程 threadA.start(); threadB.start(); } }
-
以上代码中创建两个线程,threadA和threadB,并且两个线程都休眠10秒,并且启动两个线程,这样的情况不论执行多少次都会是threadA先执行,因为threadA睡眠时并不会释放锁,依然持有独占锁资源,所以最后睡眠结束时依然是先持有锁的线程先执行。
-
另外如果在sleep(long millis)中millis参数传递了一个负数,则会抛出IllegalArgumentException异常。
-
-
join
:等待线程执行终止的方法
- 当多个线程加载资源时,需要等待所有的线程执行完毕之后再做汇总处理的情况时,就需要一个方法来控制这些线程的执行,控制所有线程执行完毕才释放线程,此时就需要用到join()方法。
public static void main(String[] args) throws InterruptedException { Thread threadA = new Thread(new Runnable() { @Override public void run() { try { Thread.sleep(1000); } catch (InterruptedException e) { e.printStackTrace(); } System.out.println("threadA over"); } }); Thread threadB = new Thread(new Runnable() { @Override public void run() { try { Thread.sleep(1000); } catch (InterruptedException e) { e.printStackTrace(); } System.out.println("threadB over"); } }); //启动子线程 threadA.start(); threadB.start(); //等待子线程执行完毕返回 threadA.join(); threadB.join(); }
- 在以上代码中启动了两个线程,分别调用了join()方法,那么当主线程执行到threadA.join()时会被阻塞,等待threadA执行完毕之后返回,因此主线程执行到join()方法时会被阻塞,等待threadA的线程执行成功后才能继续执行。同理threadB也是如此,主线程执行到threadB时也会被阻塞,等待threadB执行成功后才返回。
- 同时threadA调用threadB的join()方法时也会被阻塞,当其它线程调用threadA的interrupt()方法中断threadA时,threadA会抛出InterruptedException异常并返回。
join()方法阻塞的是当前线程
,比如主线程中执行threadA.join(),阻塞的是主线程。而不是threadA线程。
-
yield
:让出CPU执行权的方法
-
当一个线程调用yield()方法时,就是表明当前线程请求
让出自己的CPU使用权
,但是线程调度器可以忽视这个请求。 -
操作系统中线程的调度是按照时间片进行分配CPU执行权的,当一个线程使用完自己的时间片之后,线程调度器才会进行下一轮的线程调度,而当一个线程调用了yield()方法时,是告诉线程调度器自己的时间片还没用完但是不想用了,线程调度器可以进行下一轮线程调度了。
public class YieldTest implements Runnable { YieldTest(){ //创建并启动线程 Thread thread = new Thread(this); thread.start(); } @Override public void run() { for (int i = 0; i < 5; i++) { //当i=0时让出CPU执行权,放弃时间片,进行下一轮调度 if (i%5==0){ System.out.println(Thread.currentThread()+"yield"); //当前线程让出CPU执行权,放弃时间片,进行下一轮调度 Thread.yield(); } } System.out.println(Thread.currentThread()+"is over"); } public static void main(String[] args) { new YieldTest(); new YieldTest(); new YieldTest(); } }
-
以上代码中启动了三个线程并且分别在i=0 时候调用了Thread.yield()方法,所以三个线程中的输出语句并没有连在一起,因为输出第一行后当前线程就让出了CPU执行权,其它线程先使用CPU调用了其它方法。
-
-
总结:
sleep()与yield()的区别在于,当线程调用sleep()
方法时线程会被阻塞挂起到指定的时间
,这期间线程调度器并不会调度该线程
。而使用yield()
方法时,线程知识让出自己的时间片,并没有被阻塞挂起,而是处于就绪状态
,在线程调度器进行下一次调度时仍然参与线程的竞争调度,并且有可能调度到该线程。
-
-
-
线程中断
-
void interrupt()
:中断线程,当线程A正在运行时,线程B可以调用线程A的interrupt()的方法来设置线程A的中断标志为true并立即返回。设置标志仅仅是设置标志,线程A并没有被中断,它会继续执行。但是若线程A调用了wait()、join()、yield()方法被阻塞挂起时,此时线程B调用线程A的interrupt()方法,线程A会在调用这些方法的地方抛出InterruptedException异常而返回。 -
boolean isInterrupted()
:检测当前线程是否被中断,如果是则返回true,否则返回false。 -
boolean interrupted()
:检测当前线程是否被中断,如果是则返回true否则返回false。与isInterrupted()不同的是该方法如果发现当前线程被中断则会清除中断标志
,并且该方法是static
方法,可以直接通过Thread类调用,并且interrupted()内部是获取当前调用线程
的中断标志,而不是调用interrupted()方法的实例对象的中断标志。public static boolean interrupted() { return currentThread().isInterrupted(true); }
例子如下:
public class InterruptedTest { public static void main(String[] args) throws InterruptedException { Thread thread = new Thread(new Runnable() { @Override public void run() { for (;;){ } } }); //启动线程 thread.start(); //设置中断标志 thread.interrupt(); //获取中断标志 System.out.println("isInterrupted:"+thread.isInterrupted()); //获取中断标志并重置(此时获取的是主线程的中断标志) System.out.println("isInterrupted:"+ Thread.interrupted()); //获取中断标志 System.out.println("isInterrupted:"+ thread.isInterrupted()); thread.join(); } }
在第二个获取中断标志并重置的地方,此时获取的中断标志其实是主线程的中断标志,
-
-
死锁
- 概念:死锁是指两个或两个以上的线程在执行过程中,因争夺资源而造成的互相等待的现象,在没有外力的情况下,这些线程会相互等待而无法执行下去。
- 产生条件
-
互斥
:线程对已经获取到的资源会排斥其它线程的使用,也就是该资源同时只能由一个线程占用。 -
循环等待
:发生死锁时,是由一个线程请求资源的环形的链造成的。也就是t0等待t1的资源,t1等待t2资源…tn等待t0的资源。 -
不可剥夺
:线程获得资源之后就不可被其它线程抢占,除非自己使用完成。 -
占用且等待
:一个线程持有了一个资源但是又请求其它资源,而其它资源正在被其它线程占用,因此当前线程就会阻塞。public class DeadLockTest { //创建资源 private static Object objectA = new Object(); private static Object objectB = new Object(); public static void main(String[] args) { //创建线程A Thread threadA = new Thread(new Runnable() { @Override public void run() { synchronized (objectA){ System.out.println(Thread.currentThread()+"get ObjectA"); try { Thread.sleep(1000); } catch (InterruptedException e) { e.printStackTrace(); } System.out.println(Thread.currentThread()+"wait get ObjectB"); synchronized (objectB){ System.out.println(Thread.currentThread()+"get ObjectB"); } } } }); //创建线程A Thread threadB = new Thread(new Runnable() { @Override public void run() { synchronized (objectB){ System.out.println(Thread.currentThread()+"get ObjectB"); try { Thread.sleep(1000); } catch (InterruptedException e) { e.printStackTrace(); } System.out.println(Thread.currentThread()+"wait get ObjectA"); synchronized (objectA){ System.out.println(Thread.currentThread()+"get ObjectA"); } } } }); threadA.start(); threadB.start(); } }
在以上代码中线程A获取到objectA资源,线程B获取到了objectB资源。线程A休眠结束后会企图获取objectB资源,但是objectB资源正在被线程B持有,所以线程A会被阻塞而等待,而线程B休眠结束后企图获取objectA资源,objectA正在被线程A持有,所以线程A与线程B就陷入相互等待中,也就产生了死锁。
-
- 避免死锁
打破
至少一个造成死锁的条件
,但是目前只有占有和等待以及循环等待是可以打破的。- 造成死锁的原因其实和
申请资源的顺序
有关,使用资源申请的有序性原则就可以避免死锁。
-
Deamon守护线程
-
Java中的线程分为两类,分别为
deamon
线程和user
线程(用户线程),JVM启动时会调用main函数,main函数所在的线程就是守护线程,其实JVM内部还启动了好多守护线程,比如垃圾回收线程
。 -
守护线程与用户线程的区别为当最后一个
用户线程结束
时,JVM
就会正常退出
,而不管当前是否还有守护线程,也就是说守护线程不影响
JVM的退出。因此只要有一个用户线程没有结束,正常情况下JVM就不会退出。创建一个守护线程如下:
public class DaemonTest { public static void main(String[] args) { Thread daemonThread = new Thread(new Runnable() { @Override public void run() { } }); daemonThread.setDaemon(true); daemonThread.start(); } }
只需要设置线程的
daemon
参数为true即可。 -
当用户线程都执行结束之后,JVM会执行一个叫做
DestroyJava VM
的线程,该线程会等待所有用户线程结束后终止JVM进程。 -
总结
:如果你希望在主线程结束后 JVM进程马上结束,那么在创建线程时可以将其设置为守护线程,如果你希望在主线程结束之后子线程继续工作,等子线程结束之后再结束JVM进程,那么就将子线程设置为用户线程。
-
-
整理了三天的并发编程的知识,也不算整理就是看着并发编程的文章抄的,也算给自己加深学习吧!
该知识来自Java并发编程!
依然是会敲代码的汤姆猫!