并发编程入门(二):Sychronized与线程间通信

65 阅读11分钟

目录

前言

Sychronized关键字

1. 数据不一致问题

2. synchronize关键字

2.1. 同步方法&同步代码块

2.2. 锁重入

 2.3.Synchronize使用事项

2.4. 死锁

3. 小结 

线程间通信:

1.简单实现线程通信

2.等待/通知机制

3. wait和sleep的区别

4. wait和notify的其他方法

4.1.notify通知顺序

 4.2. notifyAll

4.3. wait(long)

5. 生产者和消费者(*)

5.1. 一生产一消费:操作值

 5.2.一生产一消费:操作栈

 6. 人手一支笔“ThreadLocal”

7. 小结


前言

本文是《Java并发视频入门》视频课程的笔记总结,旨在帮助更多同学入门并发编程。

本系列共五篇博客,本篇博客着重聊Sychronized关键字、线程间通信机制(Wait-Notify机制和生产者-消费者算法)。

侵权删。原视频地址:【多线程】Java并发编程入门_哔哩哔哩_bilibili本课程是Java并发编程入门版课程,适合基础薄弱的同学进行学习。如需完整版请移步腾讯课堂进行购买:https://ke.qq.com/course/3486171?tuin=5e1f405a更多优质课程请上腾讯课堂搜索“雷俊华老师”:https://ke.qq.com/course/3486171?tuin=5e1f405ahttps://www.bilibili.com/video/BV1a5411u7o7?p=1

博客汇总地址: 《并发编程入门》总结篇_勤修的博客-CSDN博客本文是《Java并发视频入门》视频课程的笔记总结,旨在帮助更多同学入门并发编程。本系列共五篇博客,此文为五篇博客的汇总篇。https://kevinhe.blog.csdn.net/article/details/125143179

Sychronized关键字

1. 数据不一致问题

/**
 * 多线程售票系统,系统十张票,四个窗口去并行售票。
 */
public class Test1 {
    //多线程问题主要是:多个线程操作同一个对象。
    public static void main(String[] args) {
        Ticket ticket = new Ticket();
        Thread t1 = new Thread(ticket, "一号窗口");
        Thread t2 = new Thread(ticket, "二号窗口");
        Thread t3 = new Thread(ticket, "三号窗口");
        Thread t4 = new Thread(ticket, "四号窗口");
        t1.start();
        t2.start();
        t3.start();
        t4.start();
    }

}

class Ticket implements Runnable {
    private int index = 0;
    private static final int MAX = 10;
    @Override
    public void run() {
      while (index < MAX) {
          try {
              TimeUnit.SECONDS.sleep(1);
          } catch (InterruptedException e) {
              e.printStackTrace();
          }
          index++;
          System.out.println(Thread.currentThread().getId() + "的号码是:" + index);
      }
    }
}
结果输出:
15的号码是:4
17的号码是:4
16的号码是:4
14的号码是:4
15的号码是:5
14的号码是:7
17的号码是:6
16的号码是:7
14的号码是:9
16的号码是:9
17的号码是:9
15的号码是:8
14的号码是:10
17的号码是:12
16的号码是:11
15的号码是:12

出现三个问题:1.号码被略过;2.某个号码重复出现;3.号码最大值超过10。

 号码跳过

 号码重复出现

 号码最大值超过10

2. synchronize关键字

2.1. 同步方法&同步代码块

数据不一致问题,究其原因是多个线程对同一对象的成员变量同时操作引起的。synchronize关键字提供排他机制,在同一时间点只有一个线程执行。

public class Test1 {
    //多线程问题主要是:多个线程操作同一个对象。
    public static void main(String[] args) {
        Ticket ticket = new Ticket();
        Thread t1 = new Thread(ticket, "一号窗口");
        Thread t2 = new Thread(ticket, "二号窗口");
        Thread t3 = new Thread(ticket, "三号窗口");
        Thread t4 = new Thread(ticket, "四号窗口");
        t1.start();
        t2.start();
        t3.start();
        t4.start();
    }

}

class Ticket implements Runnable {
    private int index = 0;
    private static final int MAX = 10;
    @Override
    public synchronized void run() {
        while (index < MAX) {
            try {
                TimeUnit.SECONDS.sleep(1);
            } catch (InterruptedException e) {
                e.printStackTrace();
            }
            index++;
            System.out.println(Thread.currentThread().getName() + "的号码是:" + index);
        }
    }
}
结果输出:
一号窗口的号码是:1
一号窗口的号码是:2
一号窗口的号码是:3
一号窗口的号码是:4
一号窗口的号码是:5
一号窗口的号码是:6
一号窗口的号码是:7
一号窗口的号码是:8
一号窗口的号码是:9
一号窗口的号码是:10

虽然以上代码解决了数据不一致问题,但仍存在两个问题:1.只有一个线程在执行;2.不够快。

解决方案如下:

class Ticket implements Runnable {
    private int index = 0;
    private static final int MAX = 10;
    private Object lock = new Object();
    @Override
    public void run() {
        while (true) {
            try {
                TimeUnit.SECONDS.sleep(1);
            } catch (InterruptedException e) {
                e.printStackTrace();
            }
            //同步代码块
            synchronized (lock) {
                if (index >= MAX) {
                    break;
                }
                index++;
                System.out.println(Thread.currentThread().getName() + "的号码是:" + index);
            }
        }
    }
}

sychronized虽然都是加锁,但加锁的对象不同:

指定加锁对象:给指定对象加锁,进入同步代码块要获取该对象的锁;

作用于实例方法:相当于对当前实例加锁,进入同步代码前要获取到该实例的锁;

作用于静态方法:相当于对当前类加锁,进入同步代码前要获取该类的锁。

2.2. 锁重入

sychronized是可重入锁,当一个线程获取到一个锁之后,再次获取到锁,也是可以获取到的,再次获取时monitor计数器加一。假设其为不可重入锁,那么在调用Method1之后,在Method1中是无法调用Method2的。

public class Test5 {
    public static void main(String[] args) {
        method1();
    }

    private static synchronized void method1() {
        System.out.println("方法一");
        method2();
    }

    private static synchronized void method2() {
        System.out.println("方法二");
        method3();
    }

    private static synchronized void method3() {
        System.out.println("方法三");
    }
}

 2.3.Synchronize使用事项

  • synchronize锁的对象为null:错误

错误,每一个对象会和moitor关联,锁的本质是为了获取monitor关联的锁。对象为null,moniot无从谈起了。

class Ticket implements Runnable {
    private Object lock = null;
    @Override
    public void run() {
        synchronized (lock) {
           //....
        }
    }
}
  • synchronize作用域太大:不推荐

synchronize有排他性,所有线程必须串行经过synchronize保护区域,如果过大,会降低效率。

  • synchronize锁了不同的对象:操作不同对象,不用加锁。

并发问题一定是指多个线程访问同一个实例,否则是绝不会出现并发问题的,锁是为了解决并发问题才存在的。假设每个线程操作的是不同对象,对象的锁也不是同一个锁,实际上还是并行代码。

  • println方法是同步的
  • 尽量不要使用String类型作为锁

字符串会从常量池查找,如果没有,则创建新对象。如果使用字符串常量作为锁,很有可能导致不同业务使用同一把锁,严重可以导致业务代码无法执行。锁一般全用Object关键字,最小化的对象。

2.4. 死锁

死锁是对锁使用不当产生的bug,当多个锁交叉使用时,很容易会产生死锁问题。如下所示:


public class Test6 {
    public static void main(String[] args) throws InterruptedException {
        DieLock dieLock = new DieLock();
        dieLock.setUserName("a");
        Thread t1 = new Thread(dieLock);
        t1.start();
        TimeUnit.MILLISECONDS.sleep(500);
        dieLock.setUserName("b");
        Thread t2 = new Thread(dieLock);
        t2.start();
    }
}

class DieLock implements Runnable {
    private String userName;

    private final Object lock1 = new Object();

    private final Object lock2 = new Object();

    public void setUserName(String userName) {
        this.userName = userName;
    }
    @Override
    public void run() {
       if (userName.equals("a")) {
           synchronized (lock1) {
               try {
                   TimeUnit.SECONDS.sleep(1);
               } catch (InterruptedException e) {
                   e.printStackTrace();
               }
               synchronized (lock2) {
                   System.out.println("userName是a");
               }
           }
       }
       if (userName.equals("b")) {
           synchronized (lock2) {
               try {
                   TimeUnit.SECONDS.sleep(1);
               } catch (InterruptedException e) {
                   e.printStackTrace();
               }
               synchronized (lock1) {
                   System.out.println("userName是b");
               }
           }
       }
    }
}

检查哪里有死锁?

1.打开IDEA的Terminal;

2.输入"jps",列出所有线程;

3.jstack -l 3460

3. 小结 

本小节主要介绍了因多线程导致的数据不一致问题(号码跳过、号码重复出现、号码超过阈值)、随后介绍了Sychronized关键字,主要从加锁对象(对象、实例方法、静态方法加锁)、锁重入、使用注意事项(加锁对象不能为null、作用域不能特别大、操作不同对象不用加锁以及尽量不要使用String类型锁),最后介绍了死锁。

线程间通信:

线程之间交换数据。

1.简单实现线程通信


public class Test1 {
    public static void main(String[] args) {
        List<String> list = new ArrayList<>();
        new Thread(new Test1Thread1(list)).start();
        new Thread(new Test1Thread2(list)).start();
    }
}

class Test1Thread1 implements Runnable {
    private List<String> list;

    public Test1Thread1(List<String> list) {
        this.list = list;
    }

    @Override
    public void run() {
        for (int i = 0; i < 10; i++) {
            list.add("aaa");
            System.out.println("添加了" + (i + 1) + "个元素");
            try {
                TimeUnit.SECONDS.sleep(1);
            } catch (InterruptedException e) {
                e.printStackTrace();
            }
        }
    }
}


class Test1Thread2 implements Runnable {
    private List<String> list;

    public Test1Thread2(List<String> list) {
        this.list = list;
    }

    @Override
    public void run() {
        while (true) {
            try {
                TimeUnit.SECONDS.sleep(4);
            } catch (InterruptedException e) {
                e.printStackTrace();
            }
            System.out.println(list.size());
            if (list.size() == 5) {
                System.out.println("集合大小超过5,线程退出");
                break;
            }
        }
    }
}
结果输出:
添加了1个元素
添加了2个元素
添加了3个元素
添加了4个元素
4
添加了5个元素
添加了6个元素
添加了7个元素
添加了8个元素
8
添加了9个元素
添加了10个元素
10
10
10

 该代码存在以下严重问题:

  1. 轮训时间很小,浪费CPU资源;
  2. 轮训时间很长,得不到想要数据;
  3. 去掉间隔时间,线程2得不到想要数据。

很容易出现可见性问题,因此需要引入等待-通知机制。

2.等待/通知机制

wait/notify机制生活中比比皆是。eg:服务员和厨师的菜品传递过程。服务员得“等待”,厨师做好菜“通知”服务员取。

wait和notify是Object的方法,因此任意类都可以调用。wait和notify必须要加到锁内,且必须持有同一把锁;执行顺序为:开始wait->开始notify->结束notify->结束wait。

public class Test2 {
    public static void main(String[] args) throws InterruptedException {
        Object lock = new Object();
        new Thread(new Test2Thread1(lock)).start();
        TimeUnit.SECONDS.sleep(3);
        new Thread(new Test2Thread2(lock)).start();
    }

}

class Test2Thread1 implements Runnable {
    private Object lock;

    public Test2Thread1(Object lock) {
        this.lock = lock;
    }

    @Override
    public void run() {
        synchronized (lock) {
            System.out.println("开始wait:" + System.currentTimeMillis());
            try {
                //执行wait,锁会得到释放。
                lock.wait();
            } catch (InterruptedException e) {
                e.printStackTrace();
            }
            System.out.println("结束wait:" + System.currentTimeMillis());
        }
    }
}



class Test2Thread2 implements Runnable {
    private Object lock;

    public Test2Thread2(Object lock) {
        this.lock = lock;
    }

    @Override
    public void run() {
        synchronized (lock) {
            System.out.println("开始notify:" + System.currentTimeMillis());
            lock.notify();
            System.out.println("结束notify:" + System.currentTimeMillis());
        }
    }
}
结果输出:
开始wait:1654358438754
开始notify:1654358441756
结束notify:1654358441757
结束wait:1654358441757

 针对简单实现线程通信的代码作如下优化:

public class Test3 {
    public static void main(String[] args) {
        List<String> list = new ArrayList<>();
        Object lock = new Object();
        new Thread(new Test3Thread1(list, lock)).start();
        new Thread(new Test3Thread2(list, lock)).start();
    }
}

class Test3Thread1 implements Runnable {
    private List<String> list;

    private Object lock = new Object();

    public Test3Thread1(List<String> list, Object lock) {
        this.list = list;
        this.lock = lock;
    }
    @Override
    public void run() {
        synchronized (lock) {
            if (list.size() != 5) {
                System.out.println("开始等待,此时list的长度为:" + list.size());
                try {
                    lock.wait();
                } catch (InterruptedException e) {
                    e.printStackTrace();
                }
                System.out.println("结束等待,此时list的长度为:" + list.size());
            }
        }
    }
}

我们发现:添加至第五个元素时,线程2发出了通知,但是线程1并未立即执行,这是因为“线程2调用notify后,锁并没有立即释放。且被notify的线程只会进入就绪状态,何时执行取决于什么时候获取到CPI的执行权”。如何解决呢?

class Test3Thread1 implements Runnable {
    private List<String> list;

    private Object lock ;

    public Test3Thread1(List<String> list, Object lock) {
        this.list = list;
        this.lock = lock;
    }
    @Override
    public void run() {
        synchronized (lock) {
            if (list.size() != 5) {
                System.out.println("开始等待,此时list的长度为:" + list.size());
                try {
                    lock.wait();
                } catch (InterruptedException e) {
                    e.printStackTrace();
                }
                System.out.println("结束等待,此时list的长度为:" + list.size());
                lock.notify();
            }
        }
    }
}


class Test3Thread2 implements Runnable {

    private List<String> list;

    private Object lock;

    public Test3Thread2(List<String> list, Object lock) {
        this.list = list;
        this.lock = lock;
    }

    @Override
    public void run() {
        synchronized (lock) {
            for (int i = 0; i < 10; i++) {
                list.add("aaa");
                System.out.println("添加了" + list.size() + "个元素");
                if (list.size() == 5) {
                    lock.notify();
                    System.out.println("满足需求,通知已经发出");
                    try {
                        lock.wait();
                    } catch (InterruptedException e) {
                        e.printStackTrace();
                    }
                }
                try {
                    TimeUnit.SECONDS.sleep(1);
                } catch (InterruptedException e) {
                    e.printStackTrace();
                }
            }
        }
    }
}
结果输出:
开始等待,此时list的长度为:0
添加了1个元素
添加了2个元素
添加了3个元素
添加了4个元素
添加了5个元素
满足需求,通知已经发出
结束等待,此时list的长度为:5
添加了6个元素
添加了7个元素
添加了8个元素
添加了9个元素
添加了10个元素

3. wait和sleep的区别

两者都会使当前线程等待。wait方法执行后,锁会立即被释放;sleep方法不会释放锁,仅仅是等待一段时间。

4. wait和notify的其他方法

4.1.notify通知顺序

只会有一个线程被通知,当多个线程等待时,会通知最先调用wait的线程。

public class Test4 {
    public static void main(String[] args) throws InterruptedException {
        Object lock = new Object();
        new Thread(new Test4Thread1(lock)).start();
        TimeUnit.SECONDS.sleep(1);
        new Thread(new Test4Thread2(lock)).start();
        TimeUnit.SECONDS.sleep(2);
        new Thread(new Test4Thread3(lock)).start();
    }
}

class Test4Thread1 implements Runnable {

    private Object lock;

    public Test4Thread1(Object lock) {
        this.lock = lock;
    }

    @Override
    public void run() {
        synchronized (lock) {
            System.out.println("线程1等待中...");
            try {
                lock.wait();
            } catch (InterruptedException e) {
                e.printStackTrace();
            }
            System.out.println("线程1被唤醒...");
        }
    }
}

class Test4Thread2 implements Runnable {

    private Object lock;

    public Test4Thread2(Object lock) {
        this.lock = lock;
    }

    @Override
    public void run() {
        synchronized (lock) {
            System.out.println("线程2等待中...");
            try {
                lock.wait();
            } catch (InterruptedException e) {
                e.printStackTrace();
            }
            System.out.println("线程2被唤醒...");
        }
    }
}

class Test4Thread3 implements Runnable {

    private Object lock;

    public Test4Thread3(Object lock) {
        this.lock = lock;
    }

    @Override
    public void run() {
        synchronized (lock) {
            System.out.println("线程3开始唤醒...");
            lock.notify();
            System.out.println("线程3唤醒完毕...");
        }
    }
}
结果输出:
线程1等待中...
线程2等待中...
线程3开始唤醒...
线程3唤醒完毕...
线程1被唤醒...

 4.2. notifyAll

上面案例,事先知道程序有两个待唤醒的线程,如果想唤醒两个线程,只需要调用两次notify即可,但实际开发中,我们并不知道有哪些线程待唤醒。使用notifyAll唤醒所有线程。notifyAll会按照后进先出算法唤醒所有wait状态的线程,即LIFO。(实际测试:JDK8后进先出,JDK15先进先出)

4.3. wait(long)

等待多久。

5. 生产者和消费者(*)

“生产者不停生产,消费者不停消费”,生产者消费者模型,是通过一个容器来解决生产者和消费者之间的强耦合问题。生产者和消费者并不直接接触,通过“货架”连接,这就是生产者-消费者模式。

5.1. 一生产一消费:操作值

使用一个值来作为货架,用来模拟“生产者和消费者”。


public class Test5 {
    //货架
    public static String value = "";

    public static void main(String[] args) {
        Object lock = new Object();
        new Thread(new Test5Produce(lock)).start();
        new Thread(new Test5Resume(lock)).start();
    }
}

//生产者
class Test5Produce implements Runnable {

    private Object lock;

    public Test5Produce(Object lock) {
        this.lock = lock;
    }


    @Override
    public void run() {
        while (true) {
            synchronized (lock) {
                // 当货架为空时,生产者要生产产品,消费者等待生产者往货架生产产品;
                // 当货架满时,消费者可以从货架拿走商品,生产者等待货架的空位。
                if (!"".equals(Test5.value)) {
                    System.out.println("生产者开始等待...");
                    try {
                        TimeUnit.SECONDS.sleep(1);
                        lock.wait();
                    } catch (InterruptedException e) {
                        e.printStackTrace();
                    }
                }
                Test5.value = System.currentTimeMillis() + "";
                System.out.println("生产者生产值为:" + Test5.value);
                try {
                    TimeUnit.SECONDS.sleep(1);
                } catch (InterruptedException e) {
                    e.printStackTrace();
                }
                lock.notify();
            }
        }
    }
}

class Test5Resume implements Runnable {
    private Object lock;

    public Test5Resume(Object lock) {
        this.lock = lock;
    }

    @Override
    public void run() {
        while (true) {
            synchronized (lock) {

                try {
                    TimeUnit.SECONDS.sleep(1);
                } catch (InterruptedException e) {
                    e.printStackTrace();
                }
                if ("".equals(Test5.value)) {
                    System.out.println("消费者开始等待...");
                    try {
                        TimeUnit.SECONDS.sleep(1);
                        lock.wait();
                    } catch (InterruptedException e) {
                        e.printStackTrace();
                    }
                }
                System.out.println("消费者获取值为:" + Test5.value);
                Test5.value = "";
                try {
                    TimeUnit.SECONDS.sleep(1);
                } catch (InterruptedException e) {
                    e.printStackTrace();
                }
                lock.notify();
            }
        }
    }
}
结果输出:
生产者生产值为:1654360356802
生产者开始等待...
消费者获取值为:1654360356802
消费者开始等待...
生产者生产值为:1654360362835
生产者开始等待...
消费者获取值为:1654360362835
.....

 5.2.一生产一消费:操作栈

“一生产一消费”的弊端是:生产完一条后,必须等待消费后才能继续生产,生产者效能低下。

饭店中,厨师负责做菜,服务员负责递菜,餐台扮演“货架”。厨师无需关心服务员给顾客递菜,仅需将做好的菜放在餐台上。

“餐台”一般使用阻塞队列来实现,在这里我们使用栈。ArrayList线程不安全,因此需要封装一个线程安全的栈。

public class MyStack<T> {
    private final List<T> list = new ArrayList<>();

    public synchronized void put(T value) {
        list.add(value);
    }

    public synchronized T pop() {
        T t = list.get(0);
        list.remove(0);
        return t;
    }

    public synchronized int size() {
        return list.size();
    }
}
public class Test7 {
    public static void main(String[] args) {
        Object lock = new Object();
        MyStack<String> myStack = new MyStack<>();
        new Thread(new Test7Produce(lock, myStack)).start();
        new Thread(new Test7Resume(lock, myStack)).start();
    }
}

class Test7Produce implements Runnable {

    private Object lock;

    private MyStack<String> stack;

    public Test7Produce(Object lock, MyStack<String> stack) {
        this.lock = lock;
        this.stack = stack;
    }

    @Override
    public void run() {
        while (true) {
            synchronized (lock) {
                if (stack.size() == 5) {
                    System.out.println("栈中数据超过5,生产者开始等待");
                    try {
                        TimeUnit.SECONDS.sleep(1);
                        lock.wait();
                    } catch (InterruptedException e) {
                        e.printStackTrace();
                    }
                }
                String value = UUID.randomUUID().toString();
                stack.put(value);
                System.out.println("生产者生产值为:" + value);
                try {
                    TimeUnit.SECONDS.sleep(1);
                } catch (InterruptedException e) {
                    e.printStackTrace();
                }
                lock.notify();
            }
        }
    }
}


class Test7Resume implements Runnable {
    private Object lock;

    private MyStack<String> stack;

    public Test7Resume(Object lock, MyStack<String> stack) {
        this.lock = lock;
        this.stack = stack;
    }

    @Override
    public void run() {
        while (true) {
            synchronized (lock) {
                if (stack.size() == 0) {
                    try {
                        System.out.println("栈中无元素,消费者开始等待");
                        TimeUnit.SECONDS.sleep(1);
                        lock.wait();
                    } catch (InterruptedException e) {
                        e.printStackTrace();
                    }
                }
                String pop = stack.pop();
                System.out.println("消费者获取元素:" + pop);
                try {
                    TimeUnit.SECONDS.sleep(1);
                } catch (InterruptedException e) {
                    e.printStackTrace();
                }
                lock.notify();
            }
        }
    }
}
结果输出:
生产者生产值为:7c050e37-1af8-476f-a40f-7bbc6eeaba97
生产者生产值为:4061a989-f89d-40a2-850e-cd89b9e69bb0
生产者生产值为:d186abfa-0647-401f-8995-8372b62f57c8
生产者生产值为:b942ecfe-2fcb-48fe-8040-333ea2f0591d
生产者生产值为:bf5d8891-6bf9-4075-bff4-821248c03f75
栈中数据超过5,生产者开始等待
消费者获取元素:7c050e37-1af8-476f-a40f-7bbc6eeaba97
消费者获取元素:4061a989-f89d-40a2-850e-cd89b9e69bb0
消费者获取元素:d186abfa-0647-401f-8995-8372b62f57c8
消费者获取元素:b942ecfe-2fcb-48fe-8040-333ea2f0591d
消费者获取元素:bf5d8891-6bf9-4075-bff4-821248c03f75
栈中无元素,消费者开始等待
生产者生产值为:804eb604-f059-4adc-ab11-fd9e4cb693e9

 6. 人手一支笔“ThreadLocal”

变量的共享可以使用public static形式去实现,所有的线程都去访问这一个变量,但是共享的过程中会出现线程安全问题。

加锁虽能解决线程安全问题,但有些场景下性能确实不高。eg:100个人填写个人信息表,假如只有一支笔,大家需要挨个去填写,必须要保证大家不会去哄抢这根笔。

ThreadLocal能够保证线程从头到尾都共享的是一个全局变量,不会加锁,也不会出现线程安全问题。

public class Test10 {
    public static void main(String[] args) {
        new Thread(new Runnable() {
            @Override
            public void run() {
                User user = GlobalUser.user;
                user.setAge(23);
                try {
                    TimeUnit.MILLISECONDS.sleep(100);
                } catch (InterruptedException e) {
                    e.printStackTrace();
                }
                user.setName("张三");
                System.out.println(user);
            }
        }).start();
        new Thread(new Runnable() {
            @Override
            public void run() {
                User user = GlobalUser.user;
                user.setAge(24);
                try {
                    TimeUnit.MILLISECONDS.sleep(120);
                } catch (InterruptedException e) {
                    e.printStackTrace();
                }
                user.setName("李四");
                System.out.println(user);
            }
        }).start();
        new Thread(new Runnable() {
            @Override
            public void run() {
                User user = GlobalUser.user;
                user.setAge(25);
                try {
                    TimeUnit.MILLISECONDS.sleep(200);
                } catch (InterruptedException e) {
                    e.printStackTrace();
                }
                user.setName("王五");
                System.out.println(user);
            }
        }).start();
        new Thread(new Runnable() {
            @Override
            public void run() {
                User user = GlobalUser.user;
                user.setAge(26);
                try {
                    TimeUnit.MILLISECONDS.sleep(180);
                } catch (InterruptedException e) {
                    e.printStackTrace();
                }
                user.setName("赵六");
                System.out.println(user);
            }
        }).start();
        new Thread(new Runnable() {
            @Override
            public void run() {
                User user = GlobalUser.user;
                user.setAge(27);
                try {
                    TimeUnit.MILLISECONDS.sleep(150);
                } catch (InterruptedException e) {
                    e.printStackTrace();
                }
                user.setName("田七");
                System.out.println(user);
            }
        }).start();
    }
}

class GlobalUser {
    public static User user = new User();
}
结果输出:
User{name='张三', age=27}
User{name='李四', age=27}
User{name='田七', age=27}
User{name='赵六', age=27}
User{name='王五', age=27}

 明显存在问题,优化后代码。


public class Test11 {
    public static void main(String[] args) {
        new Thread(new Runnable() {
            @Override
            public void run() {
                User user = Test11GlobalUser.get().get();
                user.setAge(23);
                try {
                    TimeUnit.MILLISECONDS.sleep(100);
                } catch (InterruptedException e) {
                    e.printStackTrace();
                }
                user.setName("张三");
                System.out.println(user);
            }
        }).start();
       //....
    }
}

class Test11GlobalUser {
    public static ThreadLocal<User> threadLocal = new ThreadLocal<>();

    public static ThreadLocal<User> get() {
        User user = threadLocal.get();
        if (user == null) {
            user = new User();
            threadLocal.set(user);
        }
        return threadLocal;
    }
}
结果输出:
User{name='张三', age=23}
User{name='李四', age=24}
User{name='田七', age=27}
User{name='赵六', age=26}
User{name='王五', age=25}

7. 小结

本小节我们介绍了线程通信的问题、Wait-Notify机制、Wait和Notify的其他方法,最重要的是生产者-消费者模型实现(操作值、操作栈)!最后我们介绍了ThreadLocal,它能能够保证线程从头到尾都共享的是一个全局变量,不会加锁,也不会出现线程安全问题。