原子性它提供了互斥访问,同一时刻只能有一个线程来对它进行操作。能保证同一时刻只有一个线程来对其进行操作的,除了Atomic包之外,还有锁。JDK提供锁主要分两种,synchronized是一个Java的关键字,主要是依赖JVM去实现锁,因此在这个关键字作用对象的作用范围内,都是同一时刻只能有一个线程可以进行操作的。记住是作用对象的作用范围内。
另外一种锁是JDK提供的代码层面的锁。JDK里面提供了一个叫做Lock的接口类,它主要是依赖特殊的CPU指令,实现类里面比较有代表性的是ReentrantLock。
关于Lock这个接口类的实现类,我们会在后面的课程里面单独重点地来讲,这里我们重点讲解一下synchronized这个关键字的使用。
synchronized是Java中的一个关键字,它是一种同步锁,它修饰的对象主要有四种:
第一种是修饰一个代码块,被修饰的代码称作为同步语句块,它作用的范围是大括号括起来的代码,它作用对象是调用这个代码块的对象。
修饰对象的第二种是修饰一个方法,被修饰的方法称为同步方法,它作用的范围是整个方法,作用的对象也是调用这个方法的对象。
修饰对象的第三种是修饰一个静态的方法,它作用的范围是整个静态方法。这个时候作用的对象是这个类的所有对象。
修饰对象的第四种是修饰一个类。这个时候作用的范围是synchronized后面括号括起来的部分,作用对象也是这个类的所有对象。
如果我们这里不使用线程池的话,这一个类对象两次调用了同一个方法,它们肯定本身就是同步执行的,因此我们是没法验证它们的具体的影响的。而我们加上线程池之后呢,它相当于是分别启动了两个进程去执行,然后它相当于是这个方法执行完了之后,不等这个方法执行完,立马又继续调用了一次这个方法。正好我们才能看到一个对象的两个进程同时来调用这个代码的时候它的执行情况。因此呢这里面我们就是通过了线程池以及两次调用的方式来模拟了同一个调用对象同时来调用这个方法,准确来说是这个同步代码块的执行情况。
这里对于一个修饰方法的时候,它们也是作用于当前对象的。
刚才正向的验证我们验证完了,接下来我们换不同的对象来让它乱序输出。刚才我们讲的修饰一个代码块的时候,它作用的对象是调用的对象,因此如果我们使用两个不同的对象调用同步代码块的时候,它俩互相不影响的。这里面如果我们使用线程池的话,理论上example1的test1方法的执行跟example2的test1方法它们是互相交叉执行的,而不是那种example1的test1方法执行完之后再执行example2的test1。
这个现象它就证明了对于同步代码块,它作用的是当前对象,不同调用对象之间是互相不影响的。
紧接着我们来测试一下修饰一个方法。这次的结果应该跟我们刚才演示的调用修饰一个代码块应该是很相似的,因为线程之间谁先启动谁后启动它俩是根据CPU自己来决定的,不是我们完全能控制的。example1和example2它们是交替进行执行的,这代表对于修饰一个方法也是作用于调用对象的,不同的调用对象之间是互相不影响的。
刚才我们演示了这么多,我们其实还可以额外总结出来,如果一个方法内部是一个完整的同步代码块,那么它和用synchronized的修饰的一个方法它俩是等同的,因为整个实际中需要执行的代码都是被synchronized修饰的,这个我们是可以通过我们的现象来总结出来的。大家就在理解的时候可以按照这个多会去理解,当一个方法里面整个都是一个同步代码块的时候,它跟修饰的一个方法它俩是一样的。同时呢我们额外指出一点是,如果当前这个类是个父类,如果子类继承了这个类之后呢,如果它想调用它test2的时候,它这里面是带不上synchronized的这个关键字的,这个大家一定要清楚。如果这个类是个父类,子类在继承了这个类的时候,如果调用test2,它是不包含synchronized的。原因呢是因为synchronized它不属于方法声明的一部分,这里需要大家注意一下。如果子类也想使用synchronized的话,那么它需要自己显示的在方法上面声明synchronized的才可以。修饰的代码块和修饰一个方法介绍完了,接下来介绍修饰静态方法和修饰一个类。
com.mmall.concurrency.example.sync.SynchronizedExample1
C:UsersHONGZHENHUAimoocconcurrencysrcmainjavacommmallconcurrencyexamplesyncSynchronizedExample1.java
package com.mmall.concurrency.example.sync; import lombok.extern.slf4j.Slf4j; import java.util.concurrent.ExecutorService; import java.util.concurrent.Executors; @Slf4j public class SynchronizedExample1 { // 修饰一个代码块 //public void test1() { public void test1(int j) { synchronized (this){ for (int i = 0; i < 10; i++) { //log.info("test1 - {}", i); log.info("test1 {} - {}", j, i); } } } // 修饰一个方法 //public synchronized void test2() { public synchronized void test2(int j) { for (int i = 0; i < 10; i++) { //log.info("test2 - {}", i); log.info("test2 {} - {}", j, i); } } public static void main(String args[]) { SynchronizedExample1 example1 = new SynchronizedExample1();//声明这个类的实例 SynchronizedExample1 example2 = new SynchronizedExample1();// ExecutorService executorService = Executors.newCachedThreadPool();//声明一个线程池 //开启一个进程去执行这个方法 executorService.execute(()->{ //example1.test1(); //example1.test2(); //example1.test1(1); example1.test2(1); }); executorService.execute(()->{ //example1.test1(); //example1.test2(); //example2.test1(2); example2.test2(2); }); } }
修饰一个静态方法它的作用范围是synchronized后面括起来的部分,其实就是当前这一部分。
for (int i = 0; i < 10; i++) { //log.info("test2 - {}", i); log.info("test2 {} - {}", j, i); }
然后作用的对象是这个类的所有对象,因此我们使用不同的类,它们在调用被synchronized修饰的静态方法时,同一个时间只有一个线程可以执行,因此对于当前的keys,它的运行结果我们预期是test2,然后是1,后面0-9,接下来是test2,2,0-9。
这是修饰一个静态方法的验证,接下来我们来看修饰一个类。
一个方法里面,如果它所有需要执行代码部分,都是被synchronized修饰的一个类来包围的时候,那么它和synchronized的修饰的一个静态方法它的表现是一致的。
com.mmall.concurrency.example.sync.SynchronizedExample2
C:UsersHONGZHENHUAimoocconcurrencysrcmainjavacommmallconcurrencyexamplesyncSynchronizedExample2.java
package com.mmall.concurrency.example.sync; import lombok.extern.slf4j.Slf4j; import java.util.concurrent.ExecutorService; import java.util.concurrent.Executors; @Slf4j public class SynchronizedExample2 { // 修饰一个类 //public void test1() { //public void test1(int j) { public static void test1(int j) {//这里面呢为了方便测试,我这里边也把它变成一个静态方法 //synchronized (this){ synchronized (SynchronizedExample2.class){ for (int i = 0; i < 10; i++) { //log.info("test1 - {}", i); log.info("test1 {} - {}", j, i); } } } // 修饰一个静态方法 //public synchronized void test2() { //public synchronized void test2(int j) { public static synchronized void test2(int j) { for (int i = 0; i < 10; i++) { //log.info("test2 - {}", i); log.info("test2 {} - {}", j, i); } } public static void main(String args[]) { SynchronizedExample2 example1 = new SynchronizedExample2();//声明这个类的实例 SynchronizedExample2 example2 = new SynchronizedExample2();// ExecutorService executorService = Executors.newCachedThreadPool();//声明一个线程池 //开启一个进程去执行这个方法 executorService.execute(()->{ //example1.test1(); //example1.test2(); example1.test1(1); //example1.test2(1); }); executorService.execute(()->{ //example1.test1(); //example1.test2(); example2.test1(2); //example2.test2(2); }); } }
关于synchronized的四种修饰,我们就演示完了。使用synchronized该如何保证计数是线程安全的。
我们如果是让synchronized修饰一个静态方法,那么所有类之间都是原子性操作,同一个时间只有一个线程可以执行。
因此CountExample3是一个线程安全的类。
com.mmall.concurrency.example.count.CountExample3
C:UsersHONGZHENHUAimoocconcurrencysrcmainjavacommmallconcurrencyexamplecountCountExample3.java
package com.mmall.concurrency.example.count; import com.mmall.concurrency.annoations.ThreadSafe; import lombok.extern.slf4j.Slf4j; import java.util.concurrent.CountDownLatch; import java.util.concurrent.ExecutorService; import java.util.concurrent.Executors; import java.util.concurrent.Semaphore; @Slf4j @ThreadSafe public class CountExample3 { // 请求总数 public static int clientTotal = 5000;//1000个请求 // 同时并发执行的线程数 public static int threadTotal = 200;//允许并发的线程数是50 public static int count = 0; public static void main(String[] args) throws Exception { ExecutorService executorService = Executors.newCachedThreadPool(); final Semaphore semaphore = new Semaphore(threadTotal); final CountDownLatch countDownLatch = new CountDownLatch(clientTotal); for (int i = 0; i < clientTotal; i++) { executorService.execute(()-> { try { semaphore.acquire(); add(); semaphore.release(); } catch (Exception e) { log.error("exception",e); } countDownLatch.countDown(); }); } countDownLatch.await(); executorService.shutdown(); log.info("count:{}",count); } //private static void add() { private synchronized static void add() { count++; } }
我们可以看到之前计数错误的类里面,我们把我们核心计算的方法使用synchronized修饰之后,这个类就变成线程安全的了。synchronized在这方面它使用起来还是比较简单的。
线程安全性的原子性。synchronized它是不可中断的锁,一当代码执行到synchronized作用范围之内的时候,是必须等待代码执行完的,而Lock它是可中断的锁,只要调用了unLock就可以了。synchronzied它更适合竞争不激烈的时候使用,可读性较好,Lock它在竞争激烈的时候,依然能保持常态。synchronized在竞争激烈的时候它的性能会下降的特别的快,而Atomic包它在竞争激烈时也能维持常态,性能比Lock还要好一些,但是它也有缺点,它每次只能同步一个值。
synchronized和Atomic在实际开发中我们会经常使用。