1、Java线程的状态
1. 新建状态(New):新创建了一个线程对象。
2. 就绪状态(Runnable):线程对象创建后,其他线程调用了该对象的start()方法。该状态的线程位于可运行线程池中,变得可运行,等待获取CPU的使用权。
3. 运行状态(Running):就绪状态的线程获取了CPU,执行程序代码。
4. 阻塞状态(Blocked):阻塞状态是线程因为某种原因放弃CPU使用权,暂时停止运行。直到线程进入就绪状态,才有机会转到运行状态。阻塞的情况分三种:
(一)、等待阻塞:运行的线程执行wait()方法,JVM会把该线程放入 等待池中。
(二)、同步阻塞:运行的线程在获取对象的同步锁时,若该同步锁( synchronized)被别的线程占用,则JVM会把该线程放入 锁池中。
(三)、其他阻塞:运行的线程执行sleep()或join()方法,或者发出了I/O请求时,JVM会把该线程置为阻塞状态。当sleep()状态超时、join()等待线程终止或者超时、或者I/O处理完毕时,线程重新转入就绪状态。
5. 死亡状态(Dead):线程执行完了或者因异常退出了run()方法,该线程结束生命周期。
2. 就绪状态(Runnable):线程对象创建后,其他线程调用了该对象的start()方法。该状态的线程位于可运行线程池中,变得可运行,等待获取CPU的使用权。
3. 运行状态(Running):就绪状态的线程获取了CPU,执行程序代码。
4. 阻塞状态(Blocked):阻塞状态是线程因为某种原因放弃CPU使用权,暂时停止运行。直到线程进入就绪状态,才有机会转到运行状态。阻塞的情况分三种:
(一)、等待阻塞:运行的线程执行wait()方法,JVM会把该线程放入 等待池中。
(二)、同步阻塞:运行的线程在获取对象的同步锁时,若该同步锁( synchronized)被别的线程占用,则JVM会把该线程放入 锁池中。
(三)、其他阻塞:运行的线程执行sleep()或join()方法,或者发出了I/O请求时,JVM会把该线程置为阻塞状态。当sleep()状态超时、join()等待线程终止或者超时、或者I/O处理完毕时,线程重新转入就绪状态。
5. 死亡状态(Dead):线程执行完了或者因异常退出了run()方法,该线程结束生命周期。
锁池:Java多线程中有两种同步锁synchronized和Lock,其中Lock关键字是JDK1.5之后新加入的锁,锁具有排他性,当一个线程获得锁之后,其他线程只能等待其他线程释放该锁,等待的线程也就进入了锁池。
等待池:当线程调用Object.wait()或者Condition.await()时,程序所在的线程会释放其所占有的资源(相应的会释放synchronized和Lock锁),而进入等待池,等待池当中的线程会等待其他线程调用Object.notifyAll(),Object.notify()或者 Condition.signalAll(),Condition.signal()唤醒,这样进入等待的线程就进入等待池,从等待池出来之后进入锁池,获得锁之后便可进行工作了。
需要说明的是, synchronized锁和调用wait()的对象应为同一对象!否则会报java.lang.IllegalMonitorStateException错误。正确方式如下:
public synchronized static void function04() {//类锁 try { Test05.class.wait();//本类的wait池 } catch (InterruptedException e) { e.printStackTrace(); //To change body of catch statement use File | Settings | File Templates. } } public void function02() { synchronized (lock) {//lock锁 try { lock.wait();//同样为lock锁的wait池 } catch (InterruptedException e) { e.printStackTrace(); //To change body of catch statement use File | Settings | File Templates. } } }
Java线程状态的转换图如下:
其中Thread.join()调用的是Object.wait()方法实现的,意思是让当前线程等待。 是当前调用thread1.join()的线程等待,而不是让thread1等待。
2、并发编程的思考
并发安全性的几个相关因素:可见性、顺序性、原子性。关于这三者的详细描述,见 原子性与可见性。其中原子性可以引申为互斥性,而顺序性的产生是原子性的结果即有了原子性才有了顺序性,因此以上三个因素可以推导为 可见性和互斥性。根据并发安全的特性,对synchronized关键字、volatile关键字和无锁编程(Unsafe)三种并发处理的效果如下:
| 可见性 | 互斥性 |
synchronized | 块可见 | 块互斥 |
volatile | 变量可见 | 变量互斥(无意义) |
无锁编程(Unsafe) | 变量可见 | 不保证 |
3、synchronized关键字
synchronized关键字一般情况下有以下几种用法:
/** * Created with IntelliJ IDEA. * User: yangzl2008 * Date: 14-10-25 * Time: 下午8:31 * To change this template use File | Settings | File Templates. */ public class TeshSynchronized { Object lock = new Object(); public synchronized void function01() { } public void function02() { synchronized (lock) { } } public void function03() { synchronized (this) { } } public synchronized static void function04() { } public void function05() { synchronized (TeshSynchronized.class) { } } }
对象锁,其中function01()、function02()、function03()用的是实例锁的形式,这种对象锁只对同一实例上不同线程有互斥作用。在多线程环境当中,调用同一对象的function01()、function02()、function03()是互斥的。
类锁,如function04()、function05(),这种锁对于同一类的不同线程都具有互斥作用。在多线程环境当中,调用不同对象的function04()、function05()是互斥的。
| 同一对象 | 不同对象但同一类 |
对象锁 | 多线程互斥 | 多线程不互斥 |
类锁 | 多线程互斥 | 多线程互斥 |
synchronized保证的是synchronized块级别的互斥性和可见性。
块级别的互斥性:当有一个线程获得synchronized的锁之后,其他线程不能进入这个块,而只能等获得锁的线程执行完毕之后,在进入这个块。
块级别的可见性:在多线程环境下,当一个线程进入synchronized块后,其修改的变量值在其他线程当中能够看到这个值。
基于以上以上两个特性,synchronized关键字能够保证多线程安全,这是真正意义上线程安全。
4、volatile关键字
volatile关键字,根据清英文章 聊聊并发(一)深入分析Volatile的实现原理,可知道当我们在一个变量之上volatile之后在多核处理器下会引发了两件事情。
- 将当前处理器缓存行的数据会写回到系统内存。
- 这个写回内存的操作会引起在其他CPU里缓存了该内存地址的数据无效。
volatile在多处理器开发中保证了共享变量的“可见性”。可见性的意思是当一个线程修改一个共享变量时,另外一个线程能读到这个修改的值。
/** * Created with IntelliJ IDEA. * User: yangzl2008 * Date: 14-10-26 * Time: 下午10:09 * To change this template use File | Settings | File Templates. */ public class TestVolatile { private volatile int a1; //多线程可见 private int a2; //多线程有问题 private int a3; public int getA1() { return a1; } public void setA1(int a1) { this.a1 = a1; } public int getA2() { return a2; } public void setA2(int a2) { this.a2 = a2; } public int getA3() { return a3; } public synchronized void setA3(int a3) { this.a3 = a3; } }
以上代码,我们来看看volatile的变量可见性。
对于a2,当线程调用setA2()方法对a2设值时,因为每个线程都有缓存,因此此时有可能会造成其他线程看不到新的值,而需要等到a2的同步到内存当中后,其他线程读内存时才能看到,存在多线程问题。
对于a1,因为volatile保证了a1只有一份数据在内存当中,因此,其他线程是可见的。
对于a3,因为其set方法使用synchronized 关键字,synchronized 关键字能够保证块可见性,因此其他线程是可见的。
由以上分析可知,volatile实现了synchronized 一样的多线程安全的效果。但是其实现的仅仅是可见性,对于块互斥性,并没有实现。看一下例子:
/** * Created with IntelliJ IDEA. * User: yangzl2008 * Date: 14-10-26 * Time: 下午10:21 * To change this template use File | Settings | File Templates. */ public class TestVolatile2 { volatile int count; Map<String, String> map = new ConcurrentHashMap<String, String>(); public void addContent(String key, String value) { if (count < 100) { map.put(key, value); count++; } } @Test public void testAddContent() throws Exception { ExecutorService executorService = Executors.newFixedThreadPool(10); for (int i = 0; i < 10; i++) { executorService.execute(new AddContentTask()); } // 关闭启动线程 executorService.shutdown(); // 等待子线程结束,再继续执行下面的代码 executorService.awaitTermination(Long.MAX_VALUE, TimeUnit.DAYS); System.out.println(map.size()); } private final class AddContentTask implements Runnable { @Override public void run() { //每个线程放11次 for (int i = 0; i <= 10; i++) { addContent(Thread.currentThread().getName() + " " + System.currentTimeMillis() + " " + i, "value"); } } } }以上判断count判断到达100后,就无法再向map当中放东西,但实际上,map当中的数量绝大多数情况下是大于100的。因此,volatile只能保证变量的可见性,而并不能保证块的互斥性,在某些情况下,其是无法代替synchronized的。
5、无锁编程
Java当中的无锁编程通过sun.misc.Unsafe实现的。我们以AtomicInteger源码来分析一下,其在多线程下的运作方式。首先,Unsafe通过内存偏移量得到要变量的内存位置,代码如下:
static { try { valueOffset = unsafe.objectFieldOffset (AtomicInteger.class.getDeclaredField("value")); } catch (Exception ex) { throw new Error(ex); } } private volatile int value;在我们调用getAndIncrement时,其代码如下:
public final int getAndIncrement() { for (;;) { int current = get(); int next = current + 1; if (compareAndSet(current, next)) return current; } }compareAndSet的代码如下:
public final boolean compareAndSet(int expect, int update) { return unsafe.compareAndSwapInt(this, valueOffset, expect, update); }其中unsafe.compareAndSwapInt(this, valueOffset, expect, update);是一个本地方法。
CAS (compare and swap)操作包含三个操作数 —— 内存位置(V)、预期原值(A)和新值(B)
在认为位置 V 应该包含值 A;如果包含该值,则将 B 放到这个位置;否则,不要更改该位置,只告诉我这个位置现在的值即可。他是非阻塞的。
从某种意义上来看,简单的复合操作,不管是getAndInc和getAndDec还有IncAndGet、DecAndGet等等,其实都可以归结为一个CAS操作,比如getAndIncrement,在for循环内取原值,并且+1,并且和原值比较设置结果,如果成功的话返回,否则继续。
而以上之所以会产生不成功的情况,是因为在多线程情况下,有可能有别的线程已经修改value的值,在比较的时候,value的值跟原先的值不同,因此其继续进行比较,只有在没有线程改变之后,才能修改value的值。比如,线程A打算修改value的值,但是B线程在这个时候修改了value的值,A看到value的值变量,继续下一个循环,这时,C线程又来修改了value的值,A看到后只能又进行下一个循环。因此无锁编程,无法保证顺序性,即无法保证互斥性,因为每个线程都有可能修改value的值,但是value值得修改对每个线程的修改都是可见的。
6、总结
Java多线程中的synchronized、volatile和无锁编程在不同的应用场景下,都能保证线程安全,我们在选择不同的工具时,需要根据不同的场景选择不同的工具,当然synchronized是肯定能够实现多线程安全的,但是在某些情况下,后两者的效率可能更高,这就需要我们对不同的业务场景进行仔细的分析,找到最合适的工具!
7、参考
1、 深入理解Java内存模型
3、 原子性与可见性
------------------------------本文同步发布于http://zhangsr.com/i/1114-------------------
作者:yangzl2008 发表于2014/10/25 19:29:59 原文链接
阅读:572 评论:0 查看评论