zoukankan      html  css  js  c++  java
  • Java中多线程同步

    Java使用多线程编程带来的问题就是,多线程同时读写共享变量,会出现数据不一致的问题。

    对于语句:

    n = n + 1;

    对变量的赋值操作,实际上对应三条指令:

    ILOAD  // 从内存中取出变量值
    IADD   // 对其加1操作
    ISTORE //放入变量对应的内存地址

    由于多线程的并发执行,线程2从内存中取出的值,很可能并不是线程1放入后的值。因此需要一种机制,保证线程执行这三条指令的时候,不会有其他线程干扰。待操作完成后,再将“特权”交给其他线程。

    1、使用 synchronized 关键字

    synchronized(Counter.lock) { // 获取锁
        ...
    } // 释放锁

     synchronized 使用一个对象作为锁,多个线程在执行 synchronized 下的代码时,只有获得锁之后,才能继续运行。以此来保证对共享变量的有序访问。示例如下:

    public class ThreadLock {
        public static void main(String[] args) throws InterruptedException {
            var ts = new Thread[] { new AddStudentThread(), new DesStudentThread(), new AddTeacherThread(), new DesTeacherThread() };
            for (Thread t : ts) {
                t.start();
            }
            for (Thread t : ts) {
                t.join();
            }
            System.out.println(Counter.teacherCount);
            System.out.println(Counter.studentCount);
        }
    }
    
    class Counter {
        public static final Object lockStudent = new Object();
        public static final Object lockTeacher = new Object();
        public static int studentCount = 0;
        public static int teacherCount = 0;
    }
    
    class AddStudentThread extends Thread {
        @Override
        public void run() {
            for (int i = 0; i <= 1000; i++) {
                synchronized (Counter.lockStudent) {
                    Counter.studentCount++;
                }
            }
        }
    }
    
    class DesStudentThread extends Thread {
        @Override
        public void run() {
            for (int i = 0; i <= 1000; i++) {
                synchronized (Counter.lockStudent) {
                    Counter.studentCount--;
                }
            }
        }
    }
    
    class AddTeacherThread extends Thread {
        @Override
        public void run() {
            for (int i = 0; i <= 1000; i++) {
                synchronized (Counter.lockTeacher) {
                    Counter.teacherCount++;
                }
            }
        }
    }
    
    class DesTeacherThread extends Thread {
        @Override
        public void run() {
            for (int i = 0; i <= 1000; i++) {
                synchronized (Counter.lockTeacher) {
                    Counter.teacherCount--;
                }
            }
        }
    }

    对共享变量 studentCount、teacherCount 进行操作时候,使用 synchronized 进行加锁操作,保证当前线程执行完成前,不会对共享变量访问操作。同时对于两个变量,使用了两个对象作为锁,保证执行效率。

    2、原子操作

    原子操作指的是不会被操作系统线程调度机制打断的操作。原子操作是不需要synchronized,JVM规范定义了几种原子操作:

    • 基本类型变量赋值;(long、double 未定义,x64平台是作为原子操作的实现)
    • 引用类型赋值;

    注意,多行赋值语句非原子操作,多线程同步需要加锁。

    3、同步方法

    使用synchronized,需要指定一个锁对象,同时加锁的逻辑与业务逻辑代码会混在一起,造成逻辑混乱。更好的做法是封装加锁的逻辑,外层代码只负责调用,而无需考虑线程安全。

    使用synchronized修饰方法,表示该方法是同步方法,使用this实例进行加锁。设计一个线程安全的类:

    class MyCounter {
        private int count = 0;
        
        public synchronized void add(int n) {
            this.count += n;
        }
        
        public synchronized void dec(int n) {
            this.count -= n;
        }
    }

    由于方法使用 synchronized 修饰,表示方法执行需要先获取锁(this)。

    4、线程安全

    一个类被设计为允许多线程正确访问,这个类就是“线程安全”的(thread-safe)。线程安全的类有:

    • StringBuffer
    • 不变类,StringIntegerLocalDate
    • 没有成员变量的类 Math

    一个类默认是非线程安全的。

    5、可重入锁

    可重入锁值得是一个线程重复获取同一个锁。java支持可重入锁。

    class MyCounter {
        private int count = 0;
        
        public synchronized void add(int n) {
            if(n < 0) {
                this.dec(n);
            }
            this.count += n;
        }
        
        public synchronized void dec(int n) {
            this.count -= n;
        }
    }

    在add方法中,调用dec方法,由于两个方法均使用synchronized修饰,因此在add方法内部执行dec方法,需要再次获得锁。

    6、死锁

    死锁指的是两个线程各持有对方锁,且双方均等待对方手中的锁,造成无限期等待下去。死锁发生后只能强制结束JVM进程

    一个很好理解的例子:假设有两扇门,门上两把锁,钥匙A和B。甲使用钥匙A、B可进门,乙也可以使用钥匙A和B进门。但两人同时开门,会因缺少对方手里的钥匙而相互僵持。这就是死锁。

    class Door {
        private Object lockA = new Object();
        private Object lockB = new Object();
        public void enter() {
            synchronized(lockA) {
                synchronized(lockB) {
                    //
                }
            }
        }
        
        public void goin() {
            synchronized(lockB) {
                synchronized(lockA) {
                    //
                }
            }
        }
    }

    当 enter方法和goin方法同时执行,在各自获得锁之后,又会各自等待对方手中的锁,造成无限等待。解决的方法与上述开门解锁的道理相同,要么让甲先进门,要么让乙先进。因此设置锁的顺序很重要。

    当获取A、B锁的顺序一致时,任何一方使用A锁时,另一方必须等待。不存在各自持有A、B锁的情况。

    参考链接:

    https://www.liaoxuefeng.com/wiki/1252599548343744/1306580888846370

  • 相关阅读:
    从客户端中检测到有潜在危险的 Request.Form 值
    SqlBulkCopy(批量复制)使用方法 && SqlDataAdapter Update
    SQL Server 2014 清理日志
    GitHub for Windows 2.0使用教程
    C#.Net使用正则表达式抓取百度百家文章列表
    c#设计模式-单例模式【转】
    抽象类(abstract)【转】
    C# 与Java初始化顺序及异同(转)
    [转]VisualSVN错误 Cannot query proxy blanket解决办法
    [转]TortoiseSVN客户端重新设置用户名和密码
  • 原文地址:https://www.cnblogs.com/engeng/p/15544466.html
Copyright © 2011-2022 走看看