4.9多线程--共享模式之管程

目录

  • 共享问题
  • synchronized
  • 线程安全分析
  • Monitor
  • wait / notify
  • 线程状态转换
  • Lock

Java 并发 两种解决思路:共享模型、非共享模型

image.png

共享问题

/**
 * 共享问题,由于分时系统 
 */
public class Demo1 {
    static int count = 0;
    public static void main(String[] args) throws InterruptedException {
        Thread t1 = new Thread(()->{
                for (int i = 0; i < 5000; i++) {
                    count ++;
                }
        });
        Thread t2 = new Thread(()->{
            for (int i = 0; i < 5000; i++) {
                count --;
            }
        });
        t1.start();
        t2.start();
        t1.join();
        t2.join();
        System.out.println(count);// 结果不一定是个啥
    }
}

解析:
Java 中对静态变量的自增/自减操作并不是原则操作,例如对静态变量 i ,进行 i++ 操作,实际会产生如下的 JVM 字节码指令:

getstatic i // 获取静态变量 i 的值
iconst_1   // 准备常量 1
iadd         // 自增
putstatic i // 将修改后的值存入静态变量 i 

Java 内存模型如下,完成静态变量的自增/自减需要在主存和工作内存进行数据交换。如果是单线程,字节码是顺序执行,不会交错,没有问题;


image.png

image.png

image.png

临界区 Critical Section
一段代码块内如果存在对共享资源 的多线程读写操作,称这段代码块为临界区

static int counter = 0;
static void increment(){// 临界区
  counter++;
}
static void decrement(){// 临界区
  counter;
}

竞态条件
多个线程在临界区内执行,由于代码执行序列不同而导致结果无法预测,称为竞态条件

synchronized

应用之互斥
为了避免临界区的竞态条件发生,有多种手段:

  • 阻塞式:synchronized、Lock
  • 非阻塞时:原子变量

【对象锁】,采用互斥的方式让同一时刻至多只有一个线程能持有【对象锁】,其他线程要想获取这个【对象锁】时会阻塞住(blocked)。这样就能保证拥有锁的线程可以安全的执行临界区内的代码,不用担心线程上下文切换。执行完毕后释放【对象锁】并唤醒要获得该锁的阻塞的线程。

/**
 * 共享问题 synchronized
 */
public class Demo2 {
    static int count = 0;
    static Object lock = new Object();

    public static void main(String[] args) throws InterruptedException {
        Thread t1 = new Thread(()->{
                for (int i = 0; i < 5000; i++) {
                    synchronized (lock){
                        count ++;
                    }
                }
        });
        Thread t2 = new Thread(()->{
            for (int i = 0; i < 5000; i++) {
                synchronized (lock){
                    count --;
                }
            }
        });
        t1.start();
        t2.start();
        t1.join();
        t2.join();
        System.out.println(count);// 结果是0
    }
}

image.png

image.png

image.png

思考
synchronized 实际是用 对象锁 保证了临界区内代码的原子性,临界区内的代码对外是不可分割的,不会被线程切换锁打断。

  • 如果把 synchronized(obj) 放在 for 循环的外面,如何理解?结果正确。--原子性
  • 如果 t1 synchronized(obj1) 而 t2 synchronized(obj2) 会怎样运作?--锁对象(进了不同的房间)
  • 如果 t1 synchronized(obj) 而 t2 没有加会怎样?发生上下文切换时,t2 不会去获取对象锁。
/**
 * 共享问题 synchronized 面向对象改造
 */
public class Demo3 {

    public static void main(String[] args) throws InterruptedException {
        Room room = new Room();
        Thread t1 = new Thread(()->{
            for (int i = 0; i < 5000; i++) {
                room.increament();
            }
        });
        Thread t2 = new Thread(()->{
            for (int i = 0; i < 5000; i++) {
                room.decreament();
            }
        });
        t1.start();
        t2.start();
        t1.join();
        t2.join();
        System.out.println(room.getCount());// 结果是
    }
}

class Room {
    private int count = 0;

    public void increament(){
        synchronized (this){
            count ++;
        }
    }

    public void decreament(){
        synchronized (this){
            count --;
        }
    }

    public int getCount() {
        synchronized (this){
            return count;
        }
    }
}
/**
 * 共享问题 synchronized 加在方法上
 */
public class Demo4 {
    private static int count = 0;

    //加在非静态方法上
    public synchronized void increament(){
        count ++;
    }
    //相当于
    public void increament1(){
        synchronized (this){
            count ++;
        }
    }

    //加在静态方法上
    public synchronized static void decreament(){
        count --;
    }
    //相当于
    public void decreament1(){
        synchronized (Demo4.class){
            count --;
        }
    }
}

线程八锁

考察锁的是哪个对象

/**
 * 线程八锁一:
 * 结果:1 2 或 2 1
 * 说明:synchronized 加在 Number 的非静态方法上,锁的是 this 对象,线程t1 t2 使用的同一个 n1 对象;
 */
public class Demo1 {
    public static void main(String[] args) {
        Number n1 = new Number();
        new Thread(()->{n1.a();},"t1").start();
        new Thread(()->{n1.b();},"t2").start();
    }
}

class Number {
    public synchronized void a(){
        System.out.println("1");
    }
    public synchronized void b(){
        System.out.println("2");
    }
}
/**
 * 线程八锁二:
 * 结果:睡眠1s 1 2 或 2 睡眠1s 1
 * 说明:synchronized 加在 Number 的非静态方法上,锁的是 this 对象,线程t1 t2 使用的同一个 n1 对象;
 */
public class Demo2 {
    public static void main(String[] args) {
        Number2 n1 = new Number2();
        new Thread(()->{n1.a();},"t1").start();
        new Thread(()->{n1.b();},"t2").start();
    }
}

class Number2 {
    public synchronized void a() {
        try {
            Thread.sleep(1000);
        } catch (InterruptedException e) {
            e.printStackTrace();
        }
        System.out.println("1");
    }
    public synchronized void b(){
        System.out.println("2");
    }
}
/**
 * 线程八锁三:
 * 结果:3 1s 12
 *       32/23 1s 1
 * 说明:
 */
public class Demo3 {
    public static void main(String[] args) {
        Number3 n1 = new Number3();
        new Thread(()->{n1.a();},"t1").start();
        new Thread(()->{n1.b();},"t2").start();
        new Thread(()->{n1.c();},"t3").start();
    }
}

class Number3 {
    public synchronized void a() {
        try {
            Thread.sleep(1000);
        } catch (InterruptedException e) {
            e.printStackTrace();
        }
        System.out.println("1");
    }
    public synchronized void b(){
        System.out.println("2");
    }
    public void c(){
        System.out.println("3");
    }
}
/**
 * 线程八锁四:
 * 结果:2 1s 1
 * 说明:线程 t1 t2 锁的不是同一个对象,不存在互斥,并行执行
 */
public class Demo4 {
    public static void main(String[] args) {
        Number4 n1 = new Number4();
        Number4 n2 = new Number4();
        new Thread(()->{n1.a();},"t1").start();
        new Thread(()->{n2.b();},"t2").start();
    }
}

class Number4 {
    public synchronized void a() {
        try {
            Thread.sleep(1000);
        } catch (InterruptedException e) {
            e.printStackTrace();
        }
        System.out.println("1");
    }
    public synchronized void b(){
        System.out.println("2");
    }
}
/**
 * 线程八锁五:
 * 结果:2 1s 1
 * 说明:synchronized 加在 static 方法上,锁的是Number5.class对象,所以 t1 t2 并行执行
 */
public class Demo5 {
    public static void main(String[] args) {
        Number5 n1 = new Number5();
        new Thread(()->{n1.a();},"t1").start();
        new Thread(()->{n1.b();},"t2").start();
    }
}

class Number5 {
    public synchronized static void a() {
        try {
            Thread.sleep(1000);
        } catch (InterruptedException e) {
            e.printStackTrace();
        }
        System.out.println("1");
    }
    public synchronized void b(){
        System.out.println("2");
    }
}
/**
 * 线程八锁六:
 * 结果:2 1s 1 或 1 1s 2
 * 说明:synchronized 加在 static 方法上,锁的是Number5.class对象
 */
public class Demo6 {
    public static void main(String[] args) {
        Number6 n1 = new Number6();
        new Thread(()->{n1.a();},"t1").start();
        new Thread(()->{n1.b();},"t2").start();
    }
}

class Number6 {
    public synchronized static void a() {
        try {
            Thread.sleep(1000);
        } catch (InterruptedException e) {
            e.printStackTrace();
        }
        System.out.println("1");
    }
    public synchronized static void b(){
        System.out.println("2");
    }
}
/**
 * 线程八锁七:
 * 结果:2 1s 1
 * 说明:t1 锁 Number7.class 对象,t2 锁 n2 对象
 */
public class Demo7 {
    public static void main(String[] args) {
        Number7 n1 = new Number7();
        Number7 n2 = new Number7();
        new Thread(()->{n1.a();},"t1").start();
        new Thread(()->{n2.b();},"t2").start();
    }
}

class Number7 {
    public synchronized static void a() {
        try {
            Thread.sleep(1000);
        } catch (InterruptedException e) {
            e.printStackTrace();
        }
        System.out.println("1");
    }
    public synchronized void b(){
        System.out.println("2");
    }
}
/**
 * 线程八锁八:
 * 结果:1 1s 2 或 2 1s 1
 * 说明:线程 t1 t2 锁对象都是类对象 Number8.class ,类对象只有一个,存在互斥
 */
public class Demo8 {
    public static void main(String[] args) {
        Number8 n1 = new Number8();
        Number8 n2 = new Number8();
        new Thread(()->{n1.a();},"t1").start();
        new Thread(()->{n2.b();},"t2").start();
    }
}

class Number8 {
    public synchronized static void a() {
        try {
            Thread.sleep(1000);
        } catch (InterruptedException e) {
            e.printStackTrace();
        }
        System.out.println("1");
    }
    public synchronized static void b(){
        System.out.println("2");
    }
}

线程安全分析

image.png

成员变量和静态变量是否线程安全?

  • 如果他们没有共享,则线程安全
  • 如果他们被共享了,则根据他们的状态是否能够改变,分两种情况
    • 如果只有读操作,则线程安全
    • 如果有读写操作,则这段代码是临界区,需要考虑线程安全

局部变量是否线程安全?

  • 局部变量时线程安全的
  • 但局部变量引用的对象则未必(局部变量指向的是堆中的对象,堆中对象有可能被共享)
    • 如果该对象没有逃离方法的作用访问,则是线程安全的
    • 如果该对象逃离方法的作用范围,则需要考虑线程安全问题(比如用 return 逃离)

局部变量线程安全分析

public static void test1(){
  int i = 10;
  i++;
}

每个线程调用 test1() 方法时,局部变量 i,会在每个线程的栈帧内存中被创建多分,不存在共享


image.png

image.png

局部变量的引用稍有不同

/**
 * 不安全的成员变量的例子
 */
public class Demo5 {
    static final int THREAD_NUMBER = 2;
    static final int LOOP_NUMBER = 200;
    public static void main(String[] args) {
        UnsafeThread unsafeThread = new UnsafeThread();
        for (int i = 0; i < THREAD_NUMBER; i++) {
            new Thread(()->{
                unsafeThread.method1(LOOP_NUMBER);
            },"Thread-"+(i+1)).start();
        }
    }
}

class UnsafeThread{
    ArrayList<String> list = new ArrayList<>();
    public void method1(int loopNumber){
        for (int i = 0; i < loopNumber; i++) {//临界区,存在竞态条件
            method2();
            method3();
        }
    }
    private void method2(){
        list.add("1");
    }
    private void method3(){
        list.remove(0);
    }
}
image.png
/**
 * 安全的局部变量的例子
 */
public class Demo6 {
    static final int THREAD_NUMBER = 2;
    static final int LOOP_NUMBER = 200;
    public static void main(String[] args) {
        SafeThread safeThread = new SafeThread();
        for (int i = 0; i < THREAD_NUMBER; i++) {
            new Thread(()->{
                safeThread.method1(LOOP_NUMBER);
            },"Thread-"+(i+1)).start();
        }
    }
}

class SafeThread{
    public void method1(int loopNumber){
        ArrayList<String> list = new ArrayList<>();
        for (int i = 0; i < loopNumber; i++) {//临界区,存在竞态条件
            method2(list);
            method3(list);
        }
    }
    private void method2(List<String> list){
        list.add("1");
    }
    private void method3(List<String> list){
        list.remove(0);
    }
}
image.png
/**
 * 安全的局部变量的例子
 * 修改1:将 method2 method3 的修饰符 private 改成 public
 * 情况1:在其他线程调用 method2 method3,线程安全,list 对象都是每个线程自己的,不存在共享;
 * 情况2:在 情况1 的基础上,添加 SafeThread7 的子类,重写 method3 方法。会存在 list 共享;
 */
public class Demo7 {
    static final int THREAD_NUMBER = 2;
    static final int LOOP_NUMBER = 200;
    public static void main(String[] args) {
        SafeThreadSubClass safeThread = new SafeThreadSubClass();
        for (int i = 0; i < THREAD_NUMBER; i++) {
            new Thread(()->{
                safeThread.method1(LOOP_NUMBER);
            },"Thread-"+(i+1)).start();
        }
    }
}

class SafeThread7{
    public void method1(int loopNumber){
        ArrayList<String> list = new ArrayList<>();
        for (int i = 0; i < loopNumber; i++) {//临界区,存在竞态条件
            method2(list);
            method3(list);
        }
    }
    public void method2(List<String> list){
        list.add("1");
    }
    public void method3(List<String> list){
        list.remove(0);
    }
}

class SafeThreadSubClass extends SafeThread7{
    @Override
    public void method3(List<String> list) {
        new Thread(()->{
            list.remove(0);
        }).start();
    }
}

从这个例子可以看出关键字 private 或 final 提供【安全】的意义所在,体会开闭原则中的【闭】。private 不让其他线程直接调用、不能被重写,final 不能被继承。

常见线程安全类

String Integer StringBuffer Random Vector(线程安全的 List 实现)Hashtable(线程安全的 Map 实现)java.util.concurrent 包下的类
这里说他们是线程安全的是指,多个线程调用他们同一个实例的某个方法时是安全的。他们的每个方法时原子的,但他们多个方法的组合不是原子的。

Hashtable table = new Hashtable();
//线程1 线程2
if(table.get("key") == null){
  table.put("key",value);
}
image.png
不可变类线程安全性

String、Integer 等都是不可变类,因为他们内部的状态不可以改变,因此他们的方法都是线程安全的。(replace等方法是创建返回了新对象)

实例分析
    /*
    一、servelet 在中间件 Tomcat 中只有一份存在多线程共享
     */
    public class MyServlet extends HttpServlet{
        //是安全的吗?不是,HashMap 不是线程安全的
        Map<String,Object> map = new HashMap<>();
        //是安全的吗? 是,String Integer 都是不可变类,内部状态/属性不能被改变
        String s1 = "...";
        //是安全的吗?是,String Integer 都是不可变类,内部状态/属性不能被改变
        final String s2 = "...";
        //是安全的吗?不是,Date 不是线程安全的类
        Date d1 = new Date();
        //是安全的吗?不是,final 修饰符只保证了 d2 的引用不能被改变,但他的指向对象有可能被修改
        final Date d2 = new Date();

        public void doGet(HttpServletRequest request,HttpServletRequest request){

        }
    }
    /*
    二、
     */
    public class MyServlet extends HttpServlet{
        //是安全的吗?不安全
        private UserService userService = new UserServiceImpl();

        public void doGet(HttpServletRequest request,HttpServletRequest request){

        }
    }
public class UserServiceImpl implements UserService{
  // 记录调用次数
  private int count = 0;
  public void update(){
    //临界区
    count++;
  }
}
image.png

说明:
1、spring AOP 默认都是单例模式
2、不安全
3、改进1:不用单例模式也不安全,前置时是一个对象,后置时可能又是另一个对象了
4、改进2:改成环绕通知,不用成员变量,改成局部变量

image.png

说明:
1、UserDaoImpl 中没有成员变量,不存在共享,线程安全
2、UserServiceImple 中 虽然 userDao 是成员变量,共享,但这个对象内部是无状态的,不可改变,线程安全
3、MyServlet 中 userService 是成员变量,共享,但这个对象类中的 userDao 是 private 的,不可变,线程安全

image.png

说明:
1、MyServlet userService userDao 都只有一份,存在共享,但都不可变,线程安全
2、UserDaoImpl 中,conn 是成员变量,存在共享,线程1 在执行 conn.close() 之前的操作时,发生上下文切换,线程2 执行了 conn.close() 就会发生问题

image.png

说明:
1、UserServiceImpl 中 userDao 是局部变量,每个线程都是自己的,线程安全
2、虽然线程安全,但是不推荐

image.png

虽然 sdf 是局部变量,但是可能会对外暴露,并且 SimpleDateFormat 不是线程安全的。foo 的行为是不确定的,可能导致不安全的发生,被称之为外星方法
image.png

思考:String 类 为什么是 final 的?
String 类是不可变类,每个方法都是返回新对象,是线程安全的,如果不设置成 final 的 就有可能被继承,方法被覆盖掉,变成线程不安全的。

Monitor

wait / notify

线程状态转换

Lock

©著作权归作者所有,转载或内容合作请联系作者
【社区内容提示】社区部分内容疑似由AI辅助生成,浏览时请结合常识与多方信息审慎甄别。
平台声明:文章内容(如有图片或视频亦包括在内)由作者上传并发布,文章内容仅代表作者本人观点,简书系信息发布平台,仅提供信息存储服务。

相关阅读更多精彩内容

  • Java多线程编程实战指南 核心篇 Thread类的start方法作用是启动相应的线程。启动一个线程的实质是请求J...
    纵横Top阅读 3,411评论 0 2
  • 线程同步 在大多数实际的多线程应用中, 两个或两个以上的线程需要共享对同一数据的存取。多个线程或者进程在读写一个共...
    Steven1997阅读 6,025评论 0 3
  • 进程和线程 进程 所有运行中的任务通常对应一个进程,当一个程序进入内存运行时,即变成一个进程.进程是处于运行过程中...
    胜浩_ae28阅读 10,576评论 0 23
  • 前言 1. 基本介绍 在我学习 Android 多线程优化方法的过程中,发现我对多线程优化的了解太片面。 写这篇文...
    灯不利多阅读 5,897评论 5 6
  • 1、什么是线程? 1)线程是轻量级的进程 2)线程没有独立的地址空间(内存空间) 3)线程由进程创建(寄生在进程)...
    夏与清风阅读 8,404评论 0 1

友情链接更多精彩内容