无状态对象一定是线程安全的
竞态条件
当某个计算的正确性取决于多个线程的交替执行时序时,那么久会发生竞态条件。换句话说,就是正确的结果要取决于运气。最常见的竞态条件类型就是“先检查后执行”操作,即通过一个可能失效的观测结果来决定下一步的动作。
当把变量声明为volatile类型后,编译器与运行时都会注意到这个变量是共享的,因此不会将该变量上的操作与其他内存操作一起重排序。volatile变量不会被缓存在寄存器或者对其他处理器不可见的地方,因此在读取volatile类型的变量时总会返回最新写入的值
理解volatile变量的一种有效方法是,将volatile变量的读操作和写操作分别替换成get和set方法。然而,在访问volatile变量时不会执行加锁操作,因此也就不会使执行线程阻塞,因此volatile变量是一种比synchronized关键字更轻量级的同步机制
加锁机制既可以确保可见性又可以确保原子性,而volatile变量只能确保可见性
当且仅当满足以下所有条件时,才应该使用volatile变量
- 对变量的写入操作不依赖变量当前值,或者能确保只有单个线程更新变量的值
- 该变量不会与其他状态变量一起纳入不变性条件中
- 在访问变量时不需要加锁
线程安全及不可变性
当多个线程同时访问同一个资源,并且其中的一个或者多个线程对这个资源进行了写操作,才会产生竞态条件。多个线程同时读同一个资源不会产生竞态条件。
我们可以通过创建不可变的共享对象来保证对象在线程间共享时不会被修改,从而实现线程安全。
“不变”(immutable)和“只读”(Read Only)是不同的,当一个变量是“只读”时,变量的值不能直接改变,但是可以在其他变量发生改变的时候发生改变。
引用不是线程安全的
即使一个对象是线程安全的不可变对象,指向这个对象的引用也可能不是线程安全的
线程的创建方式
Java 提供了三种创建线程的方法:
- 通过实现 Runnable 接口;
- 通过继承 Thread 类本身;
- 通过 Callable 和 Future 创建线程。
Java多线程编程
Java线程(七):Callable和Future
Java并发编程:Callable、Future和FutureTask
线程池实现
-
使用wait/notify/notifyAll实现线程间通信的几点重要说明
在Java中,可以通过配合调用Object对象的wait()方法和notify()方法或notifyAll()方法来实现线程间的通信。在线程中调用wait()方法,将阻塞等待其他线程的通知(其他线程调用notify()方法或notifyAll()方法),在线程中调用notify()方法或notifyAll()方法,将通知其他线程从wait()方法处返回。
如果线程调用了对象的wait()方法,那么线程便会处于该对象的等待池中,等待池中的线程不会去竞争该对象的锁。
当有线程调用了对象的notifyAll()方法(唤醒所有wait线程)或notify()方法(只随机唤醒一个wait线程),被唤醒的线程变回进入该对象的锁池中,锁池中的线程会去竞争该对象锁。
优先级高的线程竞争到对象锁的概率大,假若某线程没有竞争到该对象锁,它还会留在锁池中,唯有线程再次调用wait()方法,它才会重新回到等待池中。而竞争到对象锁的线程则继续往下执行,直到执行完了synchronized代码块,它会释放该对象锁,这时锁池中的线程会继续竞争该对象锁。
-
线程间通信中notify通知的遗漏(含代码)
notify通知的遗漏很容易理解,即threadA还没开始wait的时候,threadB已经notify了,这样,threadB通知是没有任何响应的,当threadB退出synchronized代码块后,threadA再开始wait,便会一直阻塞等待,直到被别的线程打断。
在使用线程的等待/通知机制时,一般都要配合一个boolean变量值(或者其他能够判断真假的条件),在notify之前改变该boolean变量的值,让wait返回后能够退出while循环(一般都要在wait方法外围加一层while循环,以防止早期通知),或在通知被遗漏后,不会被阻塞在wait方法处。这样便保证了程序的正确性。
-
线程间通信中notifyAll造成的早期通知问题(含代码)
如果线程在等待时接到通知,但线程等待的条件还不满足,此时,线程接到的就是早期通知,如果条件满足的时间很短,但很快又改变了,而变得不再满足,这时也将发生早期通知。wait的线程被notif之后从wait之后的代码开始执行,所以进入wait的条件有可能发生了改变,需要用while来判断
在使用线程的等待/通知机制时,一般都要在while循环中调用wait()方法,满足条件时,才让while循环退出,这样一般也要配合使用一个boolean变量(或其他能判断真假的条件,如本文中的list.isEmpty()),满足while循环的条件时,进入while循环,执行wait()方法,不满足while循环的条件时,跳出循环,执行后面的代码。
-
并发新特性—Lock锁和条件变量(含代码)
- 简单实用Lock锁
Java 5中引入了新的锁机制--java.util.concurrent.locks中的显式的互斥锁:Lock接口,它提供了比synchronized更加广泛的锁机制。
Lock接口有3个实现类:ReentrantLock、ReentrantReadWriteLock.ReadLock和ReentrantReadWriteLock.WriteLock,即重入锁,读锁和写锁
Lock必须被显式的创建、锁定和释放
- ReentrantLock和synchronized比较
ReentrantLock相对synchronized增加了一些高级功能,如下:
- 等待可中断:当持有锁的线程长期不释放锁时,正在等待的线程可以选择放弃等待,改为处理其他事情。而在等待由synchronized产生的互斥锁,会一直阻塞,是不能被中断
- 可实现公平锁:多个线程在等待同一个锁时,必须按照申请锁的时间顺序排队等待,而非公平锁则不保证这点,在锁释放时,任何一个等待锁的线程都有机会获得锁。synchronized中的锁是非公平锁,ReentrantLock默认情况下也是非公平锁,但可以通过构造方法ReentrantLock(true)来要求使用公平锁
- 锁可以绑定多个条件:ReentrantLock对象可以同时绑定多个Condition对象(条件变量或者条件队列)
- 读写锁
synchronized获得互斥锁不仅互斥读写操作、写写操作,还互斥读读操作,而读读操作时不会带来数据竞争,因此对读读操作也互斥的话会降低性能。Java 5中提供了读写锁,它将读锁和写锁分离,使得读读操作不互斥,获取读锁和写锁的一般形式如下:
ReadWriteLock rwl = new ReentrantReadWriteLock();
rwl.writeLock().lock(); //获取写锁
rwl.readLock().lock(); //获取读锁
- 使用ReentrantLock的最佳时机:当你需要以下高级特性时,才应该使用:可定时的、可轮询的与可中断的锁获取操作,公平队列或者非块结构的锁。否则,请使用synchronized
-
可重入锁是一种特殊的互斥锁,它可以被同一个线程多次获取,而不会产生死锁
- 首先它是互斥锁:任意时刻,只有一个线程锁。即假如A线程已经获取了锁,在A线程释放这个锁之前,B线程是无法获取这个锁的,B要获取这个锁就要进入阻塞状态
- 其次,它可以被同一个线程多次持有。即,假如A线程已经获取了这个锁,如果A线程在释放锁之前又一次请求获取这个锁,那么是能够成功的
-
Java中的可重入锁
1. synchronized
synchronized是Java提供的内置锁,是互斥锁,又是可重入锁
synchronized关键字有三种用法:
* 加在对象方法上:锁住的是当前对象,不同的对象可以被同时访问
* 加在类方法上:锁住的是当前类对应在JVM中的Class对象
* 加在代码块上:锁住的是synchronized(lockobj)中的lockobj
2. ReentrantLock
ReentrantLock是java提供的显示锁,它也是互斥锁,也是可重入锁
- 总结
- 可重入锁:即某个线程获得了锁之后,在锁释放前,它可以多次重新获取该锁
- 可重入锁解决了重入锁死的问题
- Java的内置锁synchronized和ReentrantLock都是可重入锁
ReentrantLock实现了Lock接口,并提供了与synchronized相同的互斥性和内存可见性。