虽然多线程编程极大地提高了效率,但是也会带来一定的隐患。举一个例子:我们要两个线程修改并交替打印变量a
还是这个例子,两个线程同时操作一个变量a,但是打印出来的a的最终结果一定会是200吗?答案不是。可能每次的结果都不一样。最终结果小于等于200。这就是最经典的一个线程安全问题了。两个线程操作一个变量可能两个线程同事拿到了变量的副本假设a=1,线程一,和线程二同时修改了变量副本两个线程中a=2了,这时候线程1和线程二再次将变量刷新到主内存中,等于说两个操作打印出同一个数字了,按照我们的意愿,这时候主内存中变量a应该等于3,但是还是等于2。
这个就是线程安全问题,即多个线程同时访问一个资源时,会导致程序运行结果并不是想看到的结果。。
- 由于每个线程执行的过程是不可控的,所以很可能导致最终的结果与实际上的愿望相违背或者直接导致程序出错。
如何解决线程安全问题?
基本上所有的并发模式在解决线程安全问题时,都采用“序列化访问临界资源”的方案,即在同一时刻,只能有一个线程访问临界资源,也称作同步互斥访问。
通常来说,是在访问临界资源的代码前面加上一个锁,当访问完临界资源后释放锁,让其他线程继续访问。
在Java中,提供了两种方式来实现同步互斥访问:synchronized和Lock。
-
synchronized用于多线程设计,有了synchronized关键字,多线程程序的运行结果将变得可以控制。synchronized关键字用于保护共享数据。
-
synchronized实现同步的机制:synchronized依靠"锁"机制进行多线程同步,"锁"有2种,一种是对象锁,一种是类锁。
能到达到互斥访问目的的锁。
举个例子:假设我和老王要去上厕所,但是厕所只有一个,于是我进了厕所,这时候老王就要像个SB一样在门口等待了。
在Java中,可以使用synchronized关键字来标记一个方法或者代码块,当某个线程调用该对象的synchronized方法或者访问synchronized代码块时,这个线程便获得了该对象的锁,其他线程暂时无法访问这个方法,只有等待这个方法执行完毕或者代码块执行完毕,这个线程才会释放该对象的锁,其他线程才能执行这个方法或者代码块。
在这个时候,synchronized获取的是该类的对象锁。等于说我同一个对象中的两个方法被synchronized修饰,那么这两个方法都是互斥的。第一个线程再访问的第一个方法的时候,第二个线程也必须要等待第一个线程完成了,才能访问第二个方法。
那么上面的代码加上一个synchronized。运行结果就一定了,最终打印出来的结果是200了。
synchronized {修饰代码块}的作用不仅于此,synchronized void method{}整个函数加上synchronized块,效率并不好。在函数内部,可能我们需要同步的只是小部分共享数据,其他数据,可以自由访问,这时候我们可以用 synchronized(表达式){//语句}更加精确的控制。
当修饰代码块的时候锁的就是传入的对象,this只的是当前类。
synchronized还可以修饰静态方法,这个时候锁的对象就是类对象,下面两种例子效果都相同:
synchronized是java中的一个关键字,也就是说是Java语言内置的特性。synchronized既能保证原子性,又能保证一致性。synchronized锁的同一个对象的时候,其他线程不能访问该对象中的其他的synchronized修饰的方法或者代码块,他们是互斥的。虽然synchronized可以保证线程的同步,但是在访问线程非常多的情况下,性能低下。
如果一个代码块被synchronized修饰了,当一个线程获取了对应的锁,并执行该代码块时,其他线程便只能一直等待,等待获取锁的线程释放锁,而这里获取锁的线程释放锁只会有两种情况:
-
获取锁的线程执行完了该代码块,然后线程释放对锁的占有;
-
线程执行发生异常,此时JVM会让线程自动释放锁。
那么如果这个获取锁的线程由于要等待IO或者其他原因(比如调用sleep方法)被阻塞了,但是又没有释放锁,其他线程便只能干巴巴地等待,试想一下,这多么影响程序执行效率。
因此就需要有一种机制可以不让等待的线程一直无期限地等待下去(比如只等待一定的时间或者能够响应中断),通过Lock就可以办到。
lock是一个接口,它有如下方法:
用来获取锁。如果锁已被其他线程获取,则等待。
//如果不能获取锁,则直接做其他事情用来获取锁。如果锁已被其他线程获取,则返回false,否则返回true。不会进行等待。
//如果不能获取锁,则直接做其他事情与tryLock()方法类似,只不过区别在于这个方法在拿不到锁时会等待一定的时间,在时间期限之内如果还拿不到锁,就返回false。如果如果一开始拿到锁或者在等待期间内拿到了锁,则返回true。
当通过这个方法去获取锁时,如果线程正在等待获取锁,则这个线程能够响应中断,即中断线程的等待状态。也就使说,当两个线程同时通过lock.lockInterruptibly()想获取某个锁时,假若此时线程A获取到了锁,而线程B只有在等待,那么对线程B调用threadB.interrupt()方法能够中断线程B的等待过程。
一个用来获取读锁,一个用来获取写锁。也就是说将文件的读写操作分开,分成2个锁来分配给线程,从而使得多个线程可以同时进行读操作。下面的ReentrantReadWriteLock实现了ReadWriteLock接口。
-
ReentrantReadWriteLock里面的锁主体就是一个Sync,也就是FairSync或者NonfairSync,所以说实际上只有一个锁,只是在获取读取锁和写入锁的方式上不一样。
-
如果有一个线程已经占用了读锁,则此时其他线程如果要申请写锁,则申请写锁的线程会一直等待释放读锁。但是如果其他线程要申请读锁,那么不需要等待依然能申请的到读锁。这很大的优化了性能。
-
如果有一个线程已经占用了写锁,则此时其他线程如果申请写锁或者读锁,则申请的线程会一直等待释放写锁。
- synchronized在发生异常时,会自动释放线程占有的锁,因此不会导致死锁现象发生;而Lock在发生异常时,如果没有主动通过unLock()去释放锁,则很可能造成死锁现象,因此使用Lock时需要在finally块中释放锁;
- Lock可以让等待锁的线程响应中断,而synchronized却不行,使用synchronized时,等待的线程会一直等待下去,不能够响应中断;
- 通过Lock可以知道有没有成功获取锁,而synchronized却无法办到。
- Lock可以提高多个线程进行读操作的效率。
在性能上来说,如果竞争资源不激烈,两者的性能是差不多的,而当竞争资源非常激烈时(即有大量线程同时竞争),此时Lock的性能要远远优于synchronized。所以说,在具体使用时要根据适当情况选择。
如果锁具备可重入性,则称作为可重入锁。像synchronized和ReentrantLock都是可重入锁
可中断锁:顾名思义,就是可以interrupt()中断的锁。
如果某一线程A正在执行锁中的代码,另一线程B正在等待获取该锁,可能由于等待时间过长,线程B不想等待了,想先处理其他事情,我们可以让它中断自己或者在别的线程中中断它,这种就是可中断锁。
公平锁即尽量以请求锁的顺序来获取锁。比如同是有多个线程在等待一个锁,当这个锁被释放时,等待时间最久的线程(最先请求的线程)会获得该所,这种就是公平锁。
非公平锁即无法保证锁的获取是按照请求锁的顺序进行的。这样就可能导致某个或者一些线程永远获取不到锁。
在Java中,synchronized就是非公平锁,它无法保证等待的线程获取锁的顺序。
读写锁将对一个资源(比如文件)的访问分成了2个锁,一个读锁和一个写锁。
正因为有了读写锁,才使得多个线程之间的读操作不会发生冲突,提高了程序的性能。