更多精彩文章请看:http://ctrlcv.club

在第一节1.为什么全局变量不是线程安全的?中解释了全局变量是怎么引起线程不安全问题的,那么我们在实际项目中怎么去解决线程安全问题的呢?

实例演示线程安全问题

有一点很重要,尽量不要在单例中使用全局变量,除非你能保证此单例对象不会被多线程访问,但是有时候又必须使用全局变量来统计某些东西,比如有个需求要统计某个接口的调用次数,代码如下

@Controller
@RequestMapping("/visit")
public class ArticleController {
    //记录当前用户的访问量
     private int visitCount;
     /**
     * 线程不安全 * @return
     */
     @ResponseBody
     @RequestMapping("/count")
     public String getArticleTitle(){
        String title = "文章标题";
        //....
        visitCount++;
        return "当前访问量是:" + visitCount;
     }

    /**
     * @return
     */
     @ResponseBody
     @RequestMapping("/getcount")
        public String getCount(){
            return "当前访问量是:" + visitCount;
     }
}

接下来使用我们自己写的测试并发工具类ConcurrentArticleTest来测试下100个用户(线程)访问完后,com.ctrlcv.thread.controller.ArticleController#visitCount的值,ConcurrentArticleTest代码如下,如果看不懂暂时先不用纠结细节,后面我会详细讲解这个类。

public class ConcurrentArticleTest {
    private static final int THREAD_COUNT = 100;
    private static ExecutorService threadPool = Executors.newFixedThreadPool(20);
    private static CountDownLatch countDownLatch = new CountDownLatch(THREAD_COUNT);
    public static void main(String[] args) {
      for (int i = 0; i < THREAD_COUNT; i++) {
            threadPool.execute(new Runnable() {
                @Override
                public void run() {
                    String result2 = HttpUtil.get("http://localhost:8085/visit/count");
                    System.out.println(result2);
                    countDownLatch.countDown();
                }
            });
        }
        try {
            countDownLatch.await();
            String result2 = HttpUtil.get("http://localhost:8085/visit/getcount");
            System.out.println("所有线程并发访问完成之后:" + result2);
     } catch (InterruptedException e) {
                e.printStackTrace();
     }
            threadPool.shutdown();
     }
}

image.png

可以看出最后运行完成之后输出了两个96,最大值是97,由此可以印证统计访问量这段代码不是线程安全的,接下来我们通过使用synchronized来使代码变成线程安全的。

@Controller
@RequestMapping("/visit")
public class ArticleController {
    //记录当前用户的访问量
    private int visitCount;
    private Object lock = new Object();

    /**
     * 线程安全 * @return
     */
    public String getArticleTitleSafe(){
        String title = "文章标题";
        synchronized (lock){
            visitCount++;
        }
        return title;
    }
}

注意visitCount++;我们放在了synchronized (lock){}里面,这里的意思是visitCount++;这行代码使用了同步锁synchronized,当多个线程访问这段代码的时候,同一时刻只能有一个线程在执行,其实就是把并发转成了单线程,因此也就不存在多线程环境下的线程安全问题。这里有两个问题:

  1. 为什么只把visitCount++;放在了同步代码块synchronized里面
  2. 为什么synchronized(lock)里面使用了lock对象,使用其他对象可以吗?

首先回答第一个问题,我们要搞明白是什么导致的线程安全问题,多个线程同时访问共享资源才会导致线程安全问题,那么我们解决问题,自然也就想办法从并发和共享资源两方面考虑,因为统计访问量visitCount是共享变量,所以一切针对这个变量的写操作,都有可能导致线程安全问题,所以我们就把visitCount++;放在里sync代码块里。

问题二,synchronized(lock){}表示当线程执行到synchronized的时候,线程需要获取lock对象锁,只有拿到锁才能执行代码块里面的内容,否则就进入阻塞等待,下一章节我会给大家展示如何查看synchronized导致线程阻塞时的状态

当线程A先执行到这如果发现没有其他线程拿到lock锁,那么线程A就会获取锁并且执行里面的代码,此时线程B如果执行到这,发现锁已经被其他线程获取,那线程B只能进入等待,直到线程A执行完代码块释放锁。

synchronized添加在方法上

synchronized除了加在代码块上还可以加在方法上,代码如下

public synchronized String getArticleTitleSafe(){
    String title = "文章标题";
    visitCount++;
    return title;
}

表示整个方法都是同步访问,相当于

public String getArticleTitleSafe(){
    synchronized (this){
        String title = "文章标题";
        visitCount++;
        return title;
    }    
}

可以看出直接在方法上加sync锁相当于加了this锁,表示只有当前对象互斥访问,同一个类的多个对象之间并不是互斥的

举个例子来验证下synchronized(this)只能保证当前对象线程安全,多个对象就线程不安全。

/**
 * @author jl
 * @date 2020/9/25 15:51
 */public class ThreadSafeTest {
        public static void main(String[] args) {
             ExecutorService threadPool = Executors.newFixedThreadPool(100);
             CountDownLatch countDownLatch = new CountDownLatch(200);
             Person p1 = new Person();
             Person p2 = new Person();
             for (int i = 0; i < 100; i++) {
                  threadPool.submit(()->{
                    p1.increCount();
                    countDownLatch.countDown();
                });
                threadPool.submit(()->{
                    p2.increCount();
                    countDownLatch.countDown();
                });
             }
            try {
                countDownLatch.await();//等待所有线程执行完输出变量的值
                System.out.println(p1.count);
                threadPool.shutdown();
             } catch (InterruptedException e) {
                        e.printStackTrace();
             }
        }
    }
    class Person{
        public static int count = 0;
        public synchronized void increCount(){
            count++;
     }
}

运行结果如下图
image.png
可以看到结果是199并不是200,所以synchronized(this)只能保证当前对象线程安全。注意,多线程问题并不是一定能够重现的,可能在你的机器上或者自己先后几次运行结果都不同或者相同这都是有可能的。

synchronized使用类class作为同步锁

class Student{
    public static int count = 0;
     /**
     * synchronized使用了Student.class作为同步锁,则所有的Student对象都会同步访问increCount方法 
     */ 
     public void increCount(){
        synchronized(Student.class){
         count++;
     }
   }
}

synchronized使用了Student.class作为同步锁,则所有的Student对象都会同步访问increCount方法。

更多精彩文章请看:http://ctrlcv.club


guojing
7 声望0 粉丝