synchronized、lock的简介
假设一个Integer类型的全局变量i同时被A,B,C三个线程访问,A线程主要是给i做加1的操作,B线程主要是给i做减1的操作,C线程主要是读取i的值并打印出来。那么问题来了,C线程打印的i值是没有变了,还是已经减1,或者已经加1呢?
这里就涉及到线程同步的问题,线程同步是多个线程按照预定的先后次序来运行,Java中可以通过synchronized或者lock来实现线程的同步,下面将主要介绍synchronized、lock的用法以及两者的区别。
1.synchronized
synchronized是Java中的关键字,使用synchronized能够防止多个线程同时进入并访问程序的临界区(程序的某个方法或者代码块)。synchronized可以修饰方法或者代码块,当A线程访问被synchronized修饰的方法或者代码块时,A线程就获取该对象的锁,此时如果B线程想访问该临界区,就必须等待A线程执行完毕并释放该对象的锁。
1)synchronized method():被synchronized修饰后,该方法就变成了一个同步方法,其作用范围就是整个方法,而作用对象要分两种情况来考虑。
情况一:该方法是非静态方法,其作用对象就是调用该方法的对象;
情况二:该方法是静态方法,其作用对象就是调用该方法的所有类的实例对象。
2)synchronized ():括号里可以是类或者对象。
synchronized(className.class):作用对象是访问该代码块的该类所有对象,当某个线程已经在访问该代码块时,其它该类的所有对象都不能访问该代码块。
synchronized(object):是给object加锁,其他线程访问synchronized (object)同步代码块时将会被阻塞(同一个类的不同对象可以访问该代码块)。
synchronized(this):作用对象是当前对象,其他线程访问该对象的同步方法块时将会被阻塞(同一个类的不同对象可以访问该代码块)。
下面给出一个简单例子,通过synchronized关键字,两个线程交替地输出ABABABAB字符串,代码如下:
public class PrintAB {
private final Object object = new Object();
private boolean flag = false;
public static void main(String[] args) {
PrintAB printA = new PrintAB();
MyRunnable1 myRunnable1 = printA.new MyRunnable1();
MyRunnable2 myRunnable2 = printA.new MyRunnable2();
Thread thread1 = new Thread(myRunnable1);
Thread thread2 = new Thread(myRunnable2);
thread1.start();
thread2.start();
}
public class MyRunnable1 implements Runnable {
@Override
public void run() {
while (true) {
synchronized (object) {
if (flag) {
try {
object.wait();
} catch (InterruptedException e) {
}
}
System.out.print('A');
flag = true;
object.notify();
}
}
}
}
public class MyRunnable2 implements Runnable {
@Override
public void run() {
while (true) {
synchronized (object) {
if (!flag) {
try {
object.wait();
} catch (InterruptedException e) {
}
}
System.out.print('B');
flag = false;
object.notify();
}
}
}
}
}
输出结果是:
ABABABABABAB
synchronized的更为详细的介绍可以参考Java多线程干货系列synchronized
2.Lock
synchronized是Java语言的关键字,是内置特性,而ReentrantLock是一个类(实现Lock接口的类),通过该类可以实现线程的同步。Lock是一个接口,源码很简单,主要是声明了四个方法:
public interface Lock {
void lock();
void lockInterruptibly() throws InterruptedException;
boolean tryLock();
boolean tryLock(long var1, TimeUnit var3) throws InterruptedException;
void unlock();
Condition newCondition();
}
Lock一般的使用如下:
Lock lock= ...;//获取锁
lock.lock();
try{
//处理任务
}catch(Exception e){
}finally{
lock.unlock();//释放锁
}
lock()、tryLock()、tryLock(long time, TimeUnit unit)和lockInterruptibly()是用来获取锁的,unLock()方法是用来释放锁的,其放在finally块里执行,可以保证锁一定被释放,newCondition方法下面会做介绍(通过该方法可以生成一个Condition对象,而Condition是一个多线程间协调通信的工具类)。
Lock接口的主要方法介绍:
lock():获取不到锁就不罢休,否则线程一直处于block状态。
tryLock():尝试性地获取锁,不管有没有获取到都马上返回,拿到锁就返回true,不然就返回false 。
tryLock(long time, TimeUnit unit):如果获取不到锁,就等待一段时间,超时返回false。
lockInterruptibly():该方法稍微难理解一些,在说该方法之前,先说说线程的中断机制,每个线程都有一个中断标志,不过这里要分两种情况说明:
1) 线程在sleep、wait或者join, 这个时候如果有别的线程调用该线程的 interrupt()方法,此线程会被唤醒并被要求处理InterruptedException。
2)如果线程处在运行状态, 则在调用该线程的interrupt()方法时,不会响应该中断。
lockInterruptibly()和上面的第一种情况是一样的, 线程在获取锁被阻塞时,如果调用lockInterruptibly()方法,该线程会被唤醒并被要求处理InterruptedException。下面给出一个响应中断的简单例子:
public class Test{
public static void main(String[] args){
MyRunnable myRunnable = new Test().new MyRunnable();
Thread thread1 = new Thread(myRunnable,"thread1");
Thread thread2 = new Thread(myRunnable,"thread2");
thread1.start();
thread2.start();
thread2.interrupt();
}
public class MyRunnable implements Runnable{
private Lock lock=new ReentrantLock();
@Override
public synchronized void run() {
try{
lock.lockInterruptibly();
System.out.println(Thread.currentThread().getName() +"获取了锁");
Thread.sleep(5000);
}catch(InterruptedException e) {
e.printStackTrace();
System.out.println(Thread.currentThread().getName() +"响应中断");
}
finally{
lock.unlock();
System.out.println(Thread.currentThread().getName() +"释放了锁");
}
}
}
}
输出结果是:
thread1获取了锁
thread1释放了锁
thread2响应中断
thread2在响应中断后,在finally块里执行unlock方法时,会抛出java.lang.IllegalMonitorStateException异常(因为thread2并没有获取到锁,只是在等待获取锁的时候响应了中断,这时再释放锁就会抛出异常)。
上面简单介绍了ReentrantLock的使用,下面具体介绍使用ReentrantLock的中的newCondition方法实现一个生产者消费者的例子。
生产者、消费者
例子:两个线程A、B,A生产牙刷并将其放到一个缓冲队列中,B从缓冲队列中购买(消费)牙刷(说明:缓冲队列的大小是有限制的),这样就会出现如下两种情况。
1)当缓冲队列已满时,A并不能再生产牙刷,只能等B从缓冲队列购买牙刷;
2)当缓冲队列为空时,B不能再从缓冲队列中购买牙刷,只能等A生产牙刷放到缓冲队列后才能购买。
public class ToothBrushDemo {
public static void main (String[] args){
final ToothBrushBusiness toothBrushBusiness =
new ToothBrushDemo().new ToothBrushBusiness();
new Thread(new Runnable() {
@Override
public void run() {
executeRunnable(toothBrushBusiness, true);
}
}, "牙刷生产者1").start();
new Thread(new Runnable() {
@Override
public void run() {
executeRunnable(toothBrushBusiness, false);
}
}, "牙刷消费者1").start();
}
//循环执行50次
public static void executeRunnable(ToothBrushBusiness toothBrushBusiness,
boolean isProducer){
for(int i = 0 ; i < 50 ; i++) {
if (isProducer) {
toothBrushBusiness.produceToothBrush();
} else {
toothBrushBusiness.consumeToothBrush();
}
}
}
public class ToothBrushBusiness {
//定义一个大小为10的牙刷缓冲队列
private GoodQueue<ToothBrush> toothBrushQueue = new GoodQueue<ToothBrush>(new ToothBrush[10]);
private int number = 1;
private final ReentrantLock lock = new ReentrantLock();
private final Condition notEmpty = lock.newCondition();
private final Condition notFull = lock.newCondition();
public ToothBrushBusiness() {
}
//生产牙刷
public void produceToothBrush(){
lock.lock();
try {
//牙刷缓冲队列已满,则生产牙刷线程等待
while (toothBrushQueue.isFull()){
notFull.await();
}
ToothBrush toothBrush = new ToothBrush(number);
toothBrushQueue.enQueue(toothBrush);
System.out.println("生产: " + toothBrush.toString());
number++;
//牙刷缓冲队列加入牙刷后,唤醒消费牙刷线程
notEmpty.signal();
}
catch (InterruptedException e){
e.printStackTrace();
} catch (GoodQueueException e) {
e.printStackTrace();
} finally {
lock.unlock();
}
}
//消费牙刷
public void consumeToothBrush(){
lock.lock();
try {
//牙刷缓冲队列为空,则消费牙刷线程等待
while (toothBrushQueue.isEmpty()){
notEmpty.await();
}
ToothBrush toothBrush = toothBrushQueue.deQueue();
System.out.println("消费: " + toothBrush.toString());
//从牙刷缓冲队列取出牙刷后,唤醒生产牙刷线程
notFull.signal();
}
catch (InterruptedException e){
e.printStackTrace();
} catch (GoodQueueException e) {
e.printStackTrace();
} finally {
lock.unlock();
}
}
}
public class ToothBrush {
private int number;
public ToothBrush(int number) {
this.number = number;
}
@Override
public String toString() {
return "牙刷编号{" +
"number=" + number +
'}';
}
}
}
这里缓冲队列的大小设成了10,定义了一个可重入锁lock,两个状态标记对象notEmpty,notFull,分别用来标记缓冲队列是否为空,是否已满。
1)当缓冲队列已满时,调用notFull.await方法用来阻塞生产牙刷线程。
2)当缓冲队列为空时,调用notEmpty.await方法用来阻塞购买牙刷线程。
3)notEmpty.signal用来唤醒消费牙刷线程,notFull.signal用来唤醒生产牙刷线程。
Object和Conditon对应关系如下:
Object Condition
休眠 wait await
唤醒特定线程 notify signal
唤醒所有线程 notifyAll signalAll
对于同一个锁,我们可以创建多个Condition,就是多个监视器的意思。在不同的情况下使用不同的Condition,Condition是被绑定到Lock上的,要创建一个Lock的Condition必须用newCondition()方法。
Lock锁的介绍
ReentrantLock(可重入锁)是唯一实现了Lock接口的类,并且ReentrantLock提供了更多的方法。
synchronized和ReentrantLock都是可重入锁,可重入性举个简单的例子,当一个线程执行到某个synchronized方法时,比如说method1,而在method1中会调用另外一个synchronized方法method2,此时线程不必重新去申请锁,而是可以直接执行方法method2。
上面的响应中断的例子已经地使用到了ReentrantLock,下面来介绍另外一种锁,可重入读写锁ReentrantReadWriteLock,该类实现了ReadWriteLock接口,该接口的源码如下:
public interface ReadWriteLock {
Lock readLock();
Lock writeLock();
}
ReadWriteLock接口只有获取读锁和写锁的方法,而ReentrantReadWriteLock是实现了ReadWriteLock接口,接着对其应用场景做简单介绍。
应用场景:
假设一个共享的文件,其属性是可读,如果某个时间有100个线程在同时读取该文件,如果通过synchronized或者Lock来实现线程的同步访问,那么有个问题来了,当这100个线程的某个线程获取到了锁后,其它的线程都要等该线程释放了锁才能进行读操作,这样就会造成系统资源和时间极大的浪费,而ReentrantReadWriteLock正好解决了这个问题。下面给一个简单的例子,并根据代码以及输出结果做简要说明:
public classTest{
public static voidmain(String[] args){
MyRunnable myRunnable = newTest().new MyRunnable();
Thread thread1 = new Thread(myRunnable,"thread1");
Thread thread2 = new Thread(myRunnable,"thread2");
Thread thread3 = new Thread(myRunnable,"thread3");
thread1.start();
thread2.start();
thread3.start();
}
public class MyRunnable implementsRunnable{
private ReadLocklock =new ReentrantReadWriteLock().readLock();
@Override
public synchronized void run() {
try{
lock.lock();
inti=0;
while(i<5) {
System.out.println(Thread.currentThread().getName() +"正在进行读操作");
i++;
}
System.out.println(Thread.currentThread().getName()+"读操作完毕");
}
finally{
lock.unlock();
}
}
}
}
输出结果:
thread1正在进行读操作
thread1正在进行读操作
thread1正在进行读操作
thread1正在进行读操作
thread1正在进行读操作
thread1读操作完毕
thread3正在进行读操作
thread3正在进行读操作
thread3正在进行读操作
thread3正在进行读操作
thread3正在进行读操作
thread3读操作完毕
thread2正在进行读操作
thread2正在进行读操作
thread2正在进行读操作
thread2正在进行读操作
thread2正在进行读操作
thread2读操作完毕
从输出结果可以看出,三个线程并没有交替输出,这是因为这里只是读取了5次,但将读取次数i的值改成一个较大的数值如100000时,输出结果就会交替的出现。
Lock与synchronized的比较
1)Lock是一个接口,而synchronized是Java中的关键字,synchronized是内置的语言实现;
2)synchronized在发生异常时,会自动释放线程占有的锁,因此不会导致死锁现象发生。Lock在发生异常时,如果没有主动通过unLock()方法去释放锁,则很可能造成死锁的现象,因此使用Lock时需要在finally块中释放锁;
3)Lock可以让等待锁的线程响应中断,而synchronized却不行,使用synchronized时,等待的线程会一直等待下去,不能够响应中断;
4)通过Lock可以知道有没有成功获取锁,而synchronized却无法办到;
5)Lock可以提高多个线程进行读操作的效率。
Lock和synchronized比较主要是参考Java Lock和synchronized的区别。
volatile
当一个变量被定义成volatile类型之后,该变量对所有的线程是可见的,这里的可见性指的是当一个线程修改了该变量的值,那么新值对其它线程来说是立即可知的。
虽然被volatile修饰的变量具有可见性,但是基于volatile变量的运算在并发下并不是安全的,因为Java里面的有些运算并非原子操作,下面举例说明:
public static volatile int index = 0;
public static void main(String[] args){
for(int i = 0; i < 3; i ++ ){
Thread thread = new Thread(new Runnable() {
@Override
public void run() {
for(int i = 0 ; i < 10 ; i++){
index ++;
}
}
});
thread.start();
increase();
}
System.out.println("index:" + index);
}
输出结果:
index:20
上面的代码,我们直观的反应,输出的index应该是30,而不应该是20。
其实不然,++操作不是一个原子操作,index++指令编译成字节码包含两个操作:取值,加操作。这里index被修饰成volatile,保证了此时从内存中取得的index值是正确的,但有可能其它线程通过加操作将index值加1,之前从内存中取得的index值过期了,这时候我们执行加1操作后将一个较小的index值同步到内存中。
volatile除了让变量具有可见性外,还有一个更为重要的语义:禁止指令重排优化,保证变量的赋值操作跟程序代码中的执行顺序是一致的。JVM往往会对代码进行优化,这些优化操作可能会造成程序指令在执行时会出现乱序,而volatile能够屏蔽掉JVM中必要的代码优化。
总结
很多东西觉得不用笔系统地写下来,过段时间找回就会花一定的时间,于是就写在简书上。同时也欢迎大家指正(其中有些部分是引用了其它的文章,大家也可以做下参考)。