并发与高并发(八)-线程安全性-原子性-synchronized

前言

闲暇时刻,谈一下曾经在多线程教程中接触的同步锁synchronized,相当于复习一遍吧。

主要介绍

synchronized:依赖JVM

Lock:依赖特殊的CPU指令,代码实现,ReetrantLock

主体内容

一、那么我们主要先讲解一下关于同步锁synchronized的作用范围。

1.修饰代码块:作用范围-大括号括起来的代码,作用于调用这个代码块的对象,如果不同对象调用该代码块就不会同步。

2.修饰方法:作用范围-整个方法,作用于调用这个方法的对象

3.修饰静态方法:作用范围-整个静态方法,作用于这个类的所有对象

4.修饰类:作用范围-synchronized后面括号括起来的部分,作用于这个类的所有对象(ps:两个线程调用同一个类的不同对象上的这种同步语句,也会进行同步

二、接下来,我们分别针对synchronized修饰的这四种情况写四个例子,顺便对以上的4句话作一个深入理解。

1.synchronized修饰代码块

(1)同一对象调用test

1.首先,写一个方法,让synchronized修饰代码块

@Slf4j
public class SynchronizedExample1 {
    
    public void test(String j) {
        //代码块
        synchronized(this) {
            for(int i=0;i<10;i++) {
                log.info("test-{}-{}",i,j);
            }
        }
    }
    public static void main(String[] args) {
        //同一对象
        SynchronizedExample1 se1 = new SynchronizedExample1();
        ExecutorService executorService = Executors.newCachedThreadPool();
        executorService.execute(()->{
             se1.test("线程1");
        });
        executorService.execute(()->{
            se1.test("线程2");
        });
    }
}

解释:这里我们用线程池创建了两个线程分别访问test1方法中的同步代码块,第二个线程其实不等第一个线程执行完毕,就开始去访问test1方法,但test1方法中的代码块由于第一个线程的访问上了锁,所以第二个线程不得不等待第一个线程执行完这个方法。因此执行结果为如下:

 INFO [pool-1-thread-1] - test-0-线程1
 INFO [pool-1-thread-1] - test-1-线程1
 INFO [pool-1-thread-1] - test-2-线程1
 INFO [pool-1-thread-1] - test-3-线程1
 INFO [pool-1-thread-1] - test-4-线程1
 INFO [pool-1-thread-1] - test-5-线程1
 INFO [pool-1-thread-1] - test-6-线程1
 INFO [pool-1-thread-1] - test-7-线程1
 INFO [pool-1-thread-1] - test-8-线程1
 INFO [pool-1-thread-1] - test-9-线程1
 INFO [pool-1-thread-2] - test-0-线程2
 INFO [pool-1-thread-2] - test-1-线程2
 INFO [pool-1-thread-2] - test-2-线程2
 INFO [pool-1-thread-2] - test-3-线程2
 INFO [pool-1-thread-2] - test-4-线程2
 INFO [pool-1-thread-2] - test-5-线程2
 INFO [pool-1-thread-2] - test-6-线程2
 INFO [pool-1-thread-2] - test-7-线程2
 INFO [pool-1-thread-2] - test-8-线程2
 INFO [pool-1-thread-2] - test-9-线程2

(2)不同对象调用test

@Slf4j
public class SynchronizedExample2 {
    
    public void test(String j) {
        //代码块
        synchronized(this) {
            for(int i=0;i<10;i++) {
                log.info("test-{}-{}",i,j);
            }
        }
    }
    public static void main(String[] args) {
        //不同对象
        SynchronizedExample2 se1 = new SynchronizedExample2();
        SynchronizedExample2 se2 = new SynchronizedExample2();
        ExecutorService executorService = Executors.newCachedThreadPool();
        executorService.execute(()->{
             se1.test("线程1");
        });
        executorService.execute(()->{
            se2.test("线程2");
        });
    }
}

结果我们发现,线程一和线程二都是各自随着for循环升序,互相交叉但却没有影响。这种现象就证明了同步代码块对于当前对象,不同的调用之间是互相不影响的:

 INFO [pool-1-thread-2] - test-0-线程2
 INFO [pool-1-thread-1] - test-0-线程1
 INFO [pool-1-thread-1] - test-1-线程1
 INFO [pool-1-thread-2] - test-1-线程2
 INFO [pool-1-thread-1] - test-2-线程1
 INFO [pool-1-thread-2] - test-2-线程2
 INFO [pool-1-thread-1] - test-3-线程1
 INFO [pool-1-thread-2] - test-3-线程2
 INFO [pool-1-thread-1] - test-4-线程1
 INFO [pool-1-thread-2] - test-4-线程2
 INFO [pool-1-thread-1] - test-5-线程1
 INFO [pool-1-thread-2] - test-5-线程2
 INFO [pool-1-thread-1] - test-6-线程1
 INFO [pool-1-thread-2] - test-6-线程2
 INFO [pool-1-thread-1] - test-7-线程1
 INFO [pool-1-thread-2] - test-7-线程2
 INFO [pool-1-thread-1] - test-8-线程1
 INFO [pool-1-thread-2] - test-8-线程2
 INFO [pool-1-thread-1] - test-9-线程1
 INFO [pool-1-thread-2] - test-9-线程2

2.接下来,我写一段让synchronized修饰方法的代码。

(1)同一对象调用test

@Slf4j
public class SynchronizedExample3 {
    //synchronized修饰方法
    public synchronized void test(String j) {
            for(int i=0;i<10;i++) {
                log.info("test-{}-{}",i,j);
            }
    }
    public static void main(String[] args) {
        //同一对象
        SynchronizedExample3 se1 = new SynchronizedExample3();
        ExecutorService executorService = Executors.newCachedThreadPool();
        executorService.execute(()->{
             se1.test("线程1");
        });
        executorService.execute(()->{
            se1.test("线程2");
        });
    }
}

结果为:

 INFO [pool-1-thread-1] - test-0-线程1
 INFO [pool-1-thread-1] - test-1-线程1
 INFO [pool-1-thread-1] - test-2-线程1
 INFO [pool-1-thread-1] - test-3-线程1
 INFO [pool-1-thread-1] - test-4-线程1
 INFO [pool-1-thread-1] - test-5-线程1
 INFO [pool-1-thread-1] - test-6-线程1
 INFO [pool-1-thread-1] - test-7-线程1
 INFO [pool-1-thread-1] - test-8-线程1
 INFO [pool-1-thread-1] - test-9-线程1
 INFO [pool-1-thread-2] - test-0-线程2
 INFO [pool-1-thread-2] - test-1-线程2
 INFO [pool-1-thread-2] - test-2-线程2
 INFO [pool-1-thread-2] - test-3-线程2
 INFO [pool-1-thread-2] - test-4-线程2
 INFO [pool-1-thread-2] - test-5-线程2
 INFO [pool-1-thread-2] - test-6-线程2
 INFO [pool-1-thread-2] - test-7-线程2
 INFO [pool-1-thread-2] - test-8-线程2
 INFO [pool-1-thread-2] - test-9-线程2

(2)不同对象调用test

@Slf4j
public class SynchronizedExample4 {
    //synchronized修饰方法
    public synchronized void test(String j) {
            for(int i=0;i<10;i++) {
                log.info("test-{}-{}",i,j);
            }
    }
    public static void main(String[] args) {
        //不同对象
        SynchronizedExample4 se1 = new SynchronizedExample4();
        SynchronizedExample4 se2 = new SynchronizedExample4();
        ExecutorService executorService = Executors.newCachedThreadPool();
        executorService.execute(()->{
             se1.test("线程1");
        });
        executorService.execute(()->{
            se2.test("线程2");
        });
    }
}

结果:

 INFO [pool-1-thread-1] - test-0-线程1
 INFO [pool-1-thread-1] - test-1-线程1
 INFO [pool-1-thread-1] - test-2-线程1
 INFO [pool-1-thread-1] - test-3-线程1
 INFO [pool-1-thread-2] - test-0-线程2
 INFO [pool-1-thread-2] - test-1-线程2
 INFO [pool-1-thread-2] - test-2-线程2
 INFO [pool-1-thread-1] - test-4-线程1
 INFO [pool-1-thread-2] - test-3-线程2
 INFO [pool-1-thread-1] - test-5-线程1
 INFO [pool-1-thread-2] - test-4-线程2
 INFO [pool-1-thread-1] - test-6-线程1
 INFO [pool-1-thread-1] - test-7-线程1
 INFO [pool-1-thread-2] - test-5-线程2
 INFO [pool-1-thread-1] - test-8-线程1
 INFO [pool-1-thread-2] - test-6-线程2
 INFO [pool-1-thread-1] - test-9-线程1
 INFO [pool-1-thread-2] - test-7-线程2
 INFO [pool-1-thread-2] - test-8-线程2
 INFO [pool-1-thread-2] - test-9-线程2

由此可见,修饰代码块和修饰方法的两类结果相似。

这里额外补充一点,如果父类中的方法被synchronized修饰,那么子类继承父类的时候是继承不走synchronized的,也就是说同步锁会失效,原因就是synchronized不属于方法声明的一部分。如果子类也想用synchronized,必须显式地在方法上声明synchronized才行。

3.接下来,我们用上面同样的方法测试一下被synchronized修饰的静态方法在两个线程通过两个对象的调用下的结果。

(1)同一对象调用test

@Slf4j
public class SynchronizedExample5 {
    //synchronized修饰静态方法
    public static synchronized void test(String j) {
            for(int i=0;i<10;i++) {
                log.info("test-{}-{}",i,j);
            }
    }
    public static void main(String[] args) {
        //同一对象
        SynchronizedExample5 se1 = new SynchronizedExample5();
        ExecutorService executorService = Executors.newCachedThreadPool();
        executorService.execute(()->{
             se1.test("线程1");
        });
        executorService.execute(()->{
            se1.test("线程2");
        });
    }
}

结果:

 INFO [pool-1-thread-1] - test-0-线程1
 INFO [pool-1-thread-1] - test-1-线程1
 INFO [pool-1-thread-1] - test-2-线程1
 INFO [pool-1-thread-1] - test-3-线程1
 INFO [pool-1-thread-1] - test-4-线程1
 INFO [pool-1-thread-1] - test-5-线程1
 INFO [pool-1-thread-1] - test-6-线程1
 INFO [pool-1-thread-1] - test-7-线程1
 INFO [pool-1-thread-1] - test-8-线程1
 INFO [pool-1-thread-1] - test-9-线程1
 INFO [pool-1-thread-2] - test-0-线程2
 INFO [pool-1-thread-2] - test-1-线程2
 INFO [pool-1-thread-2] - test-2-线程2
 INFO [pool-1-thread-2] - test-3-线程2
 INFO [pool-1-thread-2] - test-4-线程2
 INFO [pool-1-thread-2] - test-5-线程2
 INFO [pool-1-thread-2] - test-6-线程2
 INFO [pool-1-thread-2] - test-7-线程2
 INFO [pool-1-thread-2] - test-8-线程2
 INFO [pool-1-thread-2] - test-9-线程2

(2)不同对象调用test

@Slf4j
public class SynchronizedExample6 {
    //synchronized修饰静态方法
    public static synchronized void test(String j) {
            for(int i=0;i<10;i++) {
                log.info("test-{}-{}",i,j);
            }
    }
    public static void main(String[] args) {
        //不同对象
        SynchronizedExample6 se1 = new SynchronizedExample6();
        SynchronizedExample6 se2 = new SynchronizedExample6();
        ExecutorService executorService = Executors.newCachedThreadPool();
        executorService.execute(()->{
             se1.test("线程1");
        });
        executorService.execute(()->{
            se2.test("线程2");
        });
    }
}

结果:

 INFO [pool-1-thread-1] - test-0-线程1
 INFO [pool-1-thread-1] - test-1-线程1
 INFO [pool-1-thread-1] - test-2-线程1
 INFO [pool-1-thread-1] - test-3-线程1
 INFO [pool-1-thread-1] - test-4-线程1
 INFO [pool-1-thread-1] - test-5-线程1
 INFO [pool-1-thread-1] - test-6-线程1
 INFO [pool-1-thread-1] - test-7-线程1
 INFO [pool-1-thread-1] - test-8-线程1
 INFO [pool-1-thread-1] - test-9-线程1
 INFO [pool-1-thread-2] - test-0-线程2
 INFO [pool-1-thread-2] - test-1-线程2
 INFO [pool-1-thread-2] - test-2-线程2
 INFO [pool-1-thread-2] - test-3-线程2
 INFO [pool-1-thread-2] - test-4-线程2
 INFO [pool-1-thread-2] - test-5-线程2
 INFO [pool-1-thread-2] - test-6-线程2
 INFO [pool-1-thread-2] - test-7-线程2
 INFO [pool-1-thread-2] - test-8-线程2
 INFO [pool-1-thread-2] - test-9-线程2

发现当多个线程通过多个对象调用的时候,第二个线程是等待第一个线程执行完毕才执行。这说明什么?说明当synchronized修饰静态方法的时候作用范围于这个类的所有对象,这就是它与众不同的地方。

6.不由分说,我立马执行一下synchronized修饰类的代码,看看结果又如何?

    /**
     * 修饰一个类
     */
    public static void test1(int j){
        synchronized (SyncDecorate2.class) {
            for(int i=0;i<10;i++){
                log.info("test1-{}-{}",j,i);
            }
        }
    }
    
    public static void main(String[] args){
        //声明两个类对象,让两个线程通过两个对象分别调用各自的test1方法
        SyncDecorate2 sd1 = new SyncDecorate2();
        SyncDecorate2 sd2 = new SyncDecorate2();
        ExecutorService executorService = Executors.newCachedThreadPool();
        executorService.execute(()->{
            sd1.test1(1);
        });
        executorService.execute(()->{
            sd2.test1(2);
        });
    }

结果:

01:17:40.244 [pool-1-thread-1] INFO com.controller.synchronize.SyncDecorate2 - test1-1-0
01:17:40.247 [pool-1-thread-1] INFO com.controller.synchronize.SyncDecorate2 - test1-1-1
01:17:40.247 [pool-1-thread-1] INFO com.controller.synchronize.SyncDecorate2 - test1-1-2
01:17:40.247 [pool-1-thread-1] INFO com.controller.synchronize.SyncDecorate2 - test1-1-3
01:17:40.248 [pool-1-thread-1] INFO com.controller.synchronize.SyncDecorate2 - test1-1-4
01:17:40.248 [pool-1-thread-1] INFO com.controller.synchronize.SyncDecorate2 - test1-1-5
01:17:40.248 [pool-1-thread-1] INFO com.controller.synchronize.SyncDecorate2 - test1-1-6
01:17:40.248 [pool-1-thread-1] INFO com.controller.synchronize.SyncDecorate2 - test1-1-7
01:17:40.248 [pool-1-thread-1] INFO com.controller.synchronize.SyncDecorate2 - test1-1-8
01:17:40.248 [pool-1-thread-1] INFO com.controller.synchronize.SyncDecorate2 - test1-1-9
01:17:40.248 [pool-1-thread-2] INFO com.controller.synchronize.SyncDecorate2 - test1-2-0
01:17:40.248 [pool-1-thread-2] INFO com.controller.synchronize.SyncDecorate2 - test1-2-1
01:17:40.248 [pool-1-thread-2] INFO com.controller.synchronize.SyncDecorate2 - test1-2-2
01:17:40.248 [pool-1-thread-2] INFO com.controller.synchronize.SyncDecorate2 - test1-2-3
01:17:40.248 [pool-1-thread-2] INFO com.controller.synchronize.SyncDecorate2 - test1-2-4
01:17:40.248 [pool-1-thread-2] INFO com.controller.synchronize.SyncDecorate2 - test1-2-5
01:17:40.248 [pool-1-thread-2] INFO com.controller.synchronize.SyncDecorate2 - test1-2-6
01:17:40.248 [pool-1-thread-2] INFO com.controller.synchronize.SyncDecorate2 - test1-2-7
01:17:40.248 [pool-1-thread-2] INFO com.controller.synchronize.SyncDecorate2 - test1-2-8
01:17:40.248 [pool-1-thread-2] INFO com.controller.synchronize.SyncDecorate2 - test1-2-9

可见,修饰类的时候和修饰静态方法得到的结果是同一个道理,并没有交叉执行,而是第二个线程等待第一个执行完毕才执行。

总结

1.当synchronized修饰代码块和方法的时候,通过一个对象调用发现是线程二等待线程一执行完,锁就起了作用。但是一旦两个线程通过不同对象分别调用修饰代码块的方法和修饰方法时,出现了交叉执行的现象,代码块或方法并没有同步。这就证明了synchronized修饰代码块,修饰方法的时候作用于调用这个方法的对象。

2.当synchronized修饰静态方法和修饰类的时候,多个线程通过多个对象调用其静态方法或修饰类的时候,线程二会等待线程一执行完才执行,锁也起了作用。这就证明synchronized修饰静态方法和修饰类的时候,修饰作用于这个类的所有对象。

再回头看看那四句话,

修饰代码块:作用范围-大括号括起来的代码,作用于调用这个代码块的对象,如果不同对象调用该代码块就不会同步。

修饰方法:作用范围-整个方法,作用于调用这个方法的对象

修饰静态方法:作用范围-整个静态方法,作用于这个类的所有对象

修饰类:作用范围-synchronized后面括号括起来的部分,作用于这个类的所有对象(ps:两个线程调用同一个类的不同对象上的这种同步语句,也会进行同步

是不是有眉目了呢?

应用

以下是一个线程不安全的模拟代码。

@Slf4j
@NotThreadSafe
public class ConcurrencyTest {
    //请求数
    public static int clientTotal=5000;
    //并发数
    public static int threadTotal=200;
    //计数值
    public static int count=0;
    
    public static void main(String[] args) throws InterruptedException{
        //创建线程池
        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 {
                    //.acquire方法用于判断是否内部程序达到允许的并发量,未达到才能继续执行
                    semaphore.acquire();
                    add();
                    //.release相当于关闭信号量
                    semaphore.release();
                } catch (Exception e) {
                    log.error("exception",e);
                }
                countDownLatch.countDown();
            });
        }
        //等待计数值为0,也就是所有的过程执行完,才会继续向下执行
        countDownLatch.await();
        //关闭线程池
        executorService.shutdown();
        log.info("count:{}",count);
    }
    
    private static void add(){
        count++;
    }
}

结果应该为5000才会没有问题,但数次执行,有几次达不到5000的标准。

01:31:50.321 [main] INFO com.controller.ConcurrencyTest - count:4907

当我们在调用的静态方法前面加上synchronized,那么就变为线程安全的了。

private synchronized static void add(){
        count++;
}

结果:

01:34:37.778 [main] INFO com.controller.ConcurrencyTest - count:5000

以上就是synchronized的修饰作用讲解,如有错误,请指出。

对比

synchronized:不可中断锁,适合竞争不激烈,可读性好

lock:可中断锁,多样化同步,竞争激烈时能维持常态

Atomic:竞争激烈时能维持常态,比lock性能好;只能同步一个值

posted @ 2019-12-14 23:49  mcbbss  阅读(437)  评论(0编辑  收藏  举报