线程的状态与线程安全
线程也有生命周期,线程从创建到死亡会经历如下的过程:
创建(new)-->待运行(runnable)-->运行(running)-->死亡(dead)
|_____阻塞______|(阻塞过程包含waiting、sleeping、yeild、join、blocked)
下面将一一讲诉具体的每个过程。前面讲到怎样创建一个线程,但创建完一个线程后此线程并不会马上进入到运行状态,而是要等分配到一定的资源(比如内存空
间)后变为runnable,当调用start()方法后,线程便开始运行。如果此时cpu出于空闲,则会去执行此线程,否则就会出于blocked状态,等待cpu的执行权。这里的运行线程是调用start(),而不是run(),调用run()只是当前的线程去执行此方法,而不是开启新的线程。
waiting、sleeping、yeild、blocked都是让线程出于阻塞状态,那么彼此之间又有什么区别。首先wait()方法是让线程出于等待状态,此时线程会交出cpu的执
行权,如果线程此时持有锁还会释放出锁;sleep()同样是让线程出于睡眠状态,但是线程只会交出cpu的执行权,并不会释放手中持有的锁;yeild()是让当前运行
的线程变成待运行的状态,cpu再从同等优先级的线程中选择某个线程去执行(还是有可能会选择到此线程);join()会暂停当前线程的运行去执行调用此方法的
线程,等到调用的线程执行完毕后再回过来执行当前线程;当cpu执行其他线程时,那些没有得到执行权的线程就处于blocked状态,还有就是线程处于等待锁的时候
也处于blocked状态。
我觉得这里重点需要注意的就是wait()与notify()。用一道面试题说明这个问题。题目是:顺序输出A、B、C十次。
1 package OutputABC; 2 3 public class ABCTest1 { //三个标志位,作用是控制A、B、C输出的顺序 4 Boolean flagA = true; 5 Boolean flagB = false; 6 Boolean flagC = false; 7 Object lock = new Object(); 8 public static void main(String[] args) { 9 ABCTest1 test = new ABCTest1(); 10 test.new ThreadA().start(); 11 try { 12 Thread.sleep(1000); 13 } catch (InterruptedException e) { 14 // TODO Auto-generated catch block 15 e.printStackTrace(); 16 } 17 test.new ThreadB().start(); 18 try { 19 Thread.sleep(1000); 20 } catch (InterruptedException e) { 21 // TODO Auto-generated catch block 22 e.printStackTrace(); 23 } 24 test.new ThreadC().start(); 25 26 } 27 //线程1,输出A 28 class ThreadA extends Thread{ 29 @Override 30 public void run() { 31 // TODO Auto-generated method stub //加锁 32 synchronized(lock){ //循环十次 33 for(int i = 0;i<10;i++){ //进行判断是否该输出A了 34 if(flagA){ //输出A,同时置标志位B为true 35 System.out.println("A"); 36 flagA = false; 37 flagB = true; //通知其他线程醒来 38 lock.notifyAll(); 39 try { //自己等待 40 lock.wait(); 41 } catch (InterruptedException e) { 42 // TODO Auto-generated catch block 43 e.printStackTrace(); 44 } 45 } //如果不是就继续等待 46 else{ 47 try { 48 lock.wait(); //这个必须要,假设当输出B后,此时A被唤醒,而B会交出锁,A如果这个时候抢占到了执行权就会去执行for循环i++,很明显会使A的输出减少1次 //大家可以试一试,如果去掉了i--,此时只会输出5次 49 i--; 50 } catch (InterruptedException e) { 51 // TODO Auto-generated catch block 52 e.printStackTrace(); 53 } 54 } 55 } 56 } 57 } 58 } 59 60 class ThreadB extends Thread{ 61 @Override 62 public void run() { 63 // TODO Auto-generated method stub 64 synchronized(lock){ 65 for(int i = 0;i<10;i++){ 66 if(flagB){ 67 System.out.println("B"); 68 flagB = false; 69 flagC = true; 70 lock.notifyAll(); 71 try { 72 lock.wait(); 73 } catch (InterruptedException e) { 74 // TODO Auto-generated catch block 75 e.printStackTrace(); 76 } 77 } 78 else{ 79 try { 80 lock.wait(); 81 i--; 82 } catch (InterruptedException e) { 83 // TODO Auto-generated catch block 84 e.printStackTrace(); 85 } 86 } 87 } 88 } 89 } 90 } 91 92 class ThreadC extends Thread{ 93 // TODO Auto-generated method stub 94 @Override 95 public void run() { 96 // TODO Auto-generated method stub 97 synchronized(lock){ 98 for(int i = 0;i<10;i++){ 99 if(flagC){ 100 System.out.println("C"); 101 flagC = false; 102 flagA = true; 103 lock.notifyAll(); 104 try { 105 lock.wait(); 106 } catch (InterruptedException e) { 107 // TODO Auto-generated catch block 108 e.printStackTrace(); 109 } 110 } 111 else{ 112 try { 113 lock.wait(); 114 i--; 115 } catch (InterruptedException e) { 116 // TODO Auto-generated catch block 117 e.printStackTrace(); 118 } 119 } 120 } 121 } 122 } 123 } 124 }
这道题用wait()与notify()做我只能想到用这个标志位的比较简单,想过用三个锁分别是ABC的,但在释放锁的时候感觉好麻烦。上面的注释已经很清楚了,这里就不再赘述。
至于用notify时报错IllegalMonitorStateException这里就不再说了,在java常见错误那篇文章中有详细的说明。
------------------------------------------------------------------------------------------------------------------------------------
多线程容易产生安全问题,产生安全问题的原因就是当多个线程同时访问一个数据,并对它进行修改,此时就容易造成多次修改的危险。避免此问题的方法就是采用
同步。具体的实现有两种Lock和Synchronized,关键是需要找出需要同步的点。
1 public class SynchroTest { 2 //全局变量i,代表临街资源 3 int i = 0; 4 public static void main(String[] args) { 5 SynchroTest test = new SynchroTest(); 6 MyThraad1 my1 = test.new MyThraad1(); 7 //启动线程1,并人为让线程1进行判断后在休眠,给线程2的执行创造时间 8 my1.start(); 9 MyThraad2 my2 = test.new MyThraad2(); 10 my2.start(); 11 } 12 13 //线程1 14 class MyThraad1 extends Thread{ 15 @Override 16 public void run() { 17 if(i==0){ 18 try { 19 Thread.currentThread().sleep(5000); 20 } catch (InterruptedException e) { 21 e.printStackTrace(); 22 } 23 i++; 24 } 25 System.out.println(i); 26 } 27 } 28 //线程2 29 class MyThraad2 extends Thread{ 30 @Override 31 public void run() { 32 if(i==0){ 33 i++; 34 } 35 System.out.println(i); 36 } 37 } 38 }
采用同步关键字Synchronized,可以是同步方法,也可以是同步代码块。同步方法比较简单,只需要在需要同步的方法前面加上此关键字。但是需要注意加的必须是同一把锁
看下面这段代码,很容易出现的错误。
1 public class SynchroTest { 2 //全局变量i,代表临街资源 3 int i = 0; 4 public static void main(String[] args) { 5 SynchroTest test = new SynchroTest(); 6 MyThraad1 my1 = test.new MyThraad1(); 7 //启动线程1,并人为让线程1进行判断后在休眠,给线程2的执行创造时间 8 my1.start(); 9 MyThraad2 my2 = test.new MyThraad2(); 10 my2.start(); 11 } 12 13 //线程1 14 class MyThraad1 extends Thread{ 15 @Override 16 public synchronized void run() { 17 System.out.println(this); 18 if(i==0){ 19 try { 20 Thread.currentThread().sleep(1000); 21 } catch (InterruptedException e) { 22 e.printStackTrace(); 23 } 24 i++; 25 } 26 System.out.println(i); 27 } 28 } 29 //线程2 30 class MyThraad2 extends Thread{ 31 @Override 32 public synchronized void run() { 33 System.out.println(this); 34 if(i==0){ 35 i++; 36 } 37 System.out.println(i); 38 } 39 } 40 }
如果你没有发现错误在哪里,我们看一下打印的结果就知道了。
Thread[Thread-0,5,main]
Thread[Thread-1,5,main]
1
2
出现这个问题的原因就是不是同一把锁。最安全的方式就是自己创建锁这样就不用去判断this是谁了。
1 public class SynchroTest1 { 2 //全局变量i,代表数据库中的一个数据 3 int i = 0; 4 static SynchroTest1 test = new SynchroTest1(); 5 public static void main(String[] args) { 6 MyThraad1 my1 = test.new MyThraad1(); 7 //启动线程1,目的是修改数据库中的i 8 my1.start(); 9 //代表执行过程中的异常 10 my1.interrupt(); 11 //线程1休眠,代表之前执行过程比较慢,用户又重复执行一次 12 MyThraad2 my2 = test.new MyThraad2(); 13 my2.start(); 14 } 15 16 //线程1 17 class MyThraad1 extends Thread{ 18 @Override 19 public void run() { 20 synchronized (test) { 21 System.out.println(test); 22 if(i==0){ 23 i++; 24 } 25 System.out.println(i); 26 } 27 } 28 } 29 //线程2 30 class MyThraad2 extends Thread{ 31 @Override 32 public void run() { 33 synchronized (test) { 34 System.out.println(test); 35 if(i==0){ 36 i++; 37 } 38 System.out.println(i); 39 } 40 } 41 } 42 }
对于Lock锁还有它自己的一些特性,如它的子类ReenTrantLock,他是可以中断的,而在Synchronized中是只能等待线程自己释放锁;ReenTrantLock也有类似wait与
notify的方法,不过他是用Condition来完成的,个人觉得这个比Synchronized中的wait用起来方便一点,我们可以将锁的操作和等待操作分开。在java常见异常那篇文章中有
终端的解释,这里就来看看Condition是怎样用的。还是用上面的那个面试题,上面不是说用三个锁分别控制麻烦吗,但是在这里用Condition就可以很清晰的解决这个问题。
1 import java.util.concurrent.locks.Condition; 2 import java.util.concurrent.locks.ReentrantLock; 3 4 //试想三个线程轮流输出123共十次 5 public class ABCTest { 6 ReentrantLock lock1 = new ReentrantLock();
//A、B、C分别对应一个锁 7 Condition conA = lock1.newCondition(); 8 Condition conB = lock1.newCondition(); 9 Condition conC = lock1.newCondition(); 10 public static void main(String[] args) throws Exception { 11 ABCTest test = new ABCTest(); 12 test.new ThreadA().start(); 13 Thread.sleep(1000); 14 test.new ThreadB().start(); 15 Thread.sleep(1000); 16 test.new ThreadC().start(); 17 } 18 19 class ThreadA extends Thread{ 20 @Override 21 public void run() { 22 // TODO Auto-generated method stub 23 for(int i=0;i<10;i++){ 24 try{
//上锁 25 lock1.lock(); 26 System.out.println("A"); 27 try {
//这里是唤醒B,并让A等待,是不是好方便,压根就不用考虑锁的问题 28 conB.signal(); 29 conA.await(); 30 } catch (InterruptedException e) { 31 e.printStackTrace(); 32 } 33 34 }finally{ 35 lock1.unlock(); 36 } 37 } 38 } 39 } 40 41 class ThreadB extends Thread{ 42 @Override 43 public void run() { 44 // TODO Auto-generated method stub 45 for(int i=0;i<10;i++){ 46 try{ 47 lock1.lock(); 48 System.out.println("B"); 49 try { 50 conC.signal(); 51 conB.await(); 52 } catch (InterruptedException e) { 53 e.printStackTrace(); 54 } 55 56 }finally{ 57 lock1.unlock(); 58 } 59 } 60 } 61 } 62 63 class ThreadC extends Thread{ 64 @Override 65 public void run() { 66 // TODO Auto-generated method stub 67 for(int i = 0;i<10;i++){ 68 try{ 69 lock1.lock(); 70 System.out.println("C"); 71 try { 72 conA.signal(); 73 conC.await(); 74 } catch (InterruptedException e) { 75 e.printStackTrace(); 76 } 77 78 }finally{ 79 lock1.unlock(); 80 } 81 } 82 } 83 } 84 }
这篇文章主要讲了线程的生命周期中出现的几种状态,以及wait常见的问题;还讲了多线程出现的安全问题,以及解决办法;在最后又以一个例子讲了ReentrantLock中的Condition。