因为存在临界资源,所谓临界资源,就是统一时间只能有一个在操作的资源,比如打印机,如果同时执行多个打印任务就会错乱,临界资源在程序中就是同一时间只有一个进程或者线程访问的资源,那么怎么怎么保证统一时间只有一个线程访问了,就是加锁。
如以下这段代码,size变量就是临界资源,正常情况下,程序执行结果,size的值应该是10000,但是实际的结果会是一个小于10000的值,这就是没有加锁造成的线程安全问题。
import java.util.ArrayList; import java.util.List; import java.util.concurrent.CountDownLatch; public class ThreadUnSafeDemo { static int size; public static void main(String[] args) throws InterruptedException { // CountDownLatch的作用是尽量让线程同时开始执行 CountDownLatch countDownLatch = new CountDownLatch(1); final List<Thread> list = new ArrayList<>(10); for (int i = 0; i < 10; i++) { Thread thread = new Thread( () -> { try { countDownLatch.await(); } catch (InterruptedException e) { e.printStackTrace(); } for (int j = 0; j < 1000; j++) { size++; } }); thread.start(); list.add(thread); } countDownLatch.countDown(); // 这里等所有的线程执行完成再让主线程执行 for (Thread thread : list) { thread.join(); } // 预期结果应该10000 System.out.println(size); } }
共有三种使用方法,在静态方法上、在非静态方法上、自定义的代码块。
其中,静态方法上使用当前类的class对象当作锁对象来处理的,非静态方法上是调用该方法的对象,this,来当作锁对象处理,自定的代码块就是自己指定的对象。
在使用静态方法和普通方法上使用sychronized关键子的时候,要特别注意,锁对象是一个的话,会有很大的性能问题。
性能瓶颈的案例:
在test2执行的时候,test1是不能执行的,因为test2方法获取到了user对象的锁,test1方法要等到锁被释放。
public class Test { public static void main(String[] args) throws InterruptedException { User user = new User(); new Thread( () -> { for (; ; ) { user.test1(); } }) .start(); new Thread( () -> { try { user.test2(); } catch (InterruptedException e) { e.printStackTrace(); } }) .start(); } } class User { public synchronized void test1() { System.out.println("开始"); } public synchronized void test2() throws InterruptedException { Thread.sleep(5000); } }
就是同一个锁对象,不论是synchronized还是ReentrantLock,同一个线程可以多次持有这个锁,就是获取到了这个锁对象之后,如果再一次遇到了这个锁对象同步的资源,依然可以进入。
那么为什么要这么设计呢,因为我们在日常开发的过程中,方法里面大多数都是方法调用方法的,不能说我这个方法获取到了锁,到下一个方法同一个锁我还要等着锁被释放,这个锁本来就是我持有,那就死锁了。所以synchronized、ReentrantLock都是“重入锁”。
public class ReentrantDemo { final Object lock = new Object(); private void method1() { synchronized (lock) { // do something method2(); } } private void method2() { // 如果锁不是重入的,在这就会永远获取不到锁,就会发生死锁。 synchronized (lock) { // do something } } }
锁升级(不可逆):
在jdk1.6之前,sychronized的性能是没有lock锁好的,因为sychronized锁,之前上来就是重量级锁,是依赖协程的,要和os进行交互,效率不是很高,jdk1.6之后,首先上的锁是偏向锁,根据后面对锁资源的竞争程度,依次升级为轻量级锁和重量级锁。其中偏向锁,基本上没有锁的竞争。轻量级锁是锁竞争很小,释放锁的时间也很短,不用释放cpu的执行权,而重量级锁是处理锁的竞争很激烈的情况的。
在jdk1.6之前sychronized的性能要低于ReentrantLock的,在jdk1.6之后做了锁升级的优化之后性能就和ReentrantLock差不多,但是ReentrantLock的功能要多一些。
具体现在的锁是什么级别,是存储在对象头中的。
锁粗化:
加锁和解锁也是要消耗资源的,如果存在一连串的加锁和解锁操作,可能会造成不必要的性能损耗,这时候会将这些锁拓展成一个更大的锁,避免频繁的加锁和解锁操作。
// 优化前 class User { private final Object lock = new Object(); public void test() { synchronized (lock) { System.out.println("print 1"); } synchronized (lock) { System.out.println("print 2"); } synchronized (lock) { System.out.println("print 3"); } } } // 优化后 class User { private final Object lock = new Object(); public void test() { synchronized (lock) { System.out.println("print 1"); System.out.println("print 2"); System.out.println("print 3"); } } }
锁消除:
Java虚拟机在JIT编译时(可以简单理解为当某段代码即将第一次被执行时进行编译,又称即时编译),通过对运行上下文的扫描,经过逃逸分析,去除不可能存在共享资源竞争的锁,通过这种方式消除没有必要的锁,可以节省毫无意义的请求锁时间。
class User { public void test() { final Object lock = new Object(); // 这个锁是没有意义的,就会被消除掉 synchronized (lock) { System.out.println("print 1"); System.out.println("print 2"); System.out.println("print 3"); } } }
可见性:
sychronized是可以保证线程的可见性的,因为获取锁的时候,线程会把临界资源拷贝到自己的工作内存中去,等到释放锁的时候再把资源刷新回主内存中,这个操作过程中,其他线程一直在阻塞着,所以保证了共享变量内存可见性。