在Java语言中写出线程安全的程序

  • synchronized对象监视器为Object
  • synchronized对象监视器为Class
  • volatile作用
  • synchronized和volatile区别及使用

synchronized同步方法

保障预案自信、可见性和有序性

  • 方法体内部的私有变量不存在线程安全问题
  • 全局实例变量会存在线程安全问题

全局实例变量存在线程安全问题

如果多个线程同时操作同一个对象内部的实例变量,则有可能出现线程安全问题。

  • 解决办法
    1.多个线程访问的对象都不相同则不会有问题
    2.在需要调用的方法上加上synchronized,此时会锁住该方法所属的对象,而不是锁该方法
    也就是说如果有多个对象实例,则synchronized锁住的是多个对象,每个对象之间不会冲突,无需等待。
    而如果多个线程操作同一个对象内的synchronized方法时,则需要等待

同步synchronized原理

其在字节码文件中会加入flag访问修饰符ACC_SYNCHRONIZED来控制访问权限,告知此方法是同步方法,再使用monitorenter进入和monitorexit退出指令来进行同步处理,具体字节码文件可在我的博客地址查看
http://jtao.work/archives/lei-wen-jian-jie-gou
image.png

synchronized总结

一个对象有A、B两个方法,都为非static。2个线程分别调用A、B方法

  • 如果A加锁、B未加锁,则可以异步同时调用
  • 如果A、B都加锁,则另一个线程只能等待先得到对象锁的线程释放锁之后才能调用,即锁等待
  • synchronized并不是锁方法,而是锁对象。哪个线程拿到这个锁,就可以访问这个锁对象中的synchronized方法

脏读

一个对象里面的set方法加了synchronized,但get方法没有加锁。那么多线程时,线程A使用get,线程B使用set。有可能get得到的值是set之前的,真实的值已经改变。解决的办法是在get方法上也加上synchronized锁住。

synchronized锁重入

即在使用synchronized时,一个线程拥有了对象锁之后,再次请求此对象锁调用其他方法是可以的,即synchronized方法内部调用本类的其他synchronized方法是永远可以得到对象锁的,synchronized是可重入锁
image.png

  • 这种情况在子类和父类也存在,可以理解为继承
    image.png
  • 子类如果继承父类的方法时重写没有加入synchronized关键字则不会加锁

出现异常时,锁会自动释放

当出现异常时,对象的锁会自动释放,让其他线程进入。
注意:Thread类中的suspend()方法和sleep()方法被调用后并不会释放锁

判断对象是否被上锁

public static native boolean holdsLock(Object obj);

image.png
image.png

synchronized同步代码块,一半异步,一半同步

有时候因为加了synchronized锁,所以当多个线程调用同一个方法时那么必须等待该方法执行完成,或许这很耗时,没有达到多线程的目的。此时可以根据具体的业务逻辑区分方法体内哪些必须串行执行,哪些可以异步执行

//同步代码块

/**
 * @author jtao
 */
class E {
    void doLongTimeTask() {
        for (int i = 0; i < 100; i++) {
            System.out.println("未加synchronized锁  ThreadName=" + Thread.currentThread().getName() + "  i=" + i);
        }
        System.out.println(" ");
        synchronized (this) {
            for (int i = 0; i < 100; i++) {
                System.out.println("加synchronized锁  ThreadName=" + Thread.currentThread().getName() + i);
            }
        }
    }

    public static void main(String[] args) {
        new Thread(() -> new E().doLongTimeTask()).start();
        new Thread(() -> new E().doLongTimeTask()).start();
    }
}

此时没有在方法上面加上synchronized,而是在方法体内的代码块中加入synchronized
未加锁的代码块交替执行
image.png
加synchronized锁的代码必然不能交替执行
image.png
image.png

  • 注意其synchronized锁的还是对象,当加锁时,另一个线程调用同一个对象的其他synchronized方法或者代码块不能够被使用

println()方法也是synchronized锁this对象

image.png
这样多线程情况下不会因为PrintStream对象被修改后导致打印内容混乱

synchronized锁任意对象

synchronized除了锁this对象外还可以锁对象内的实例变量和其他对象,此时和锁this的对象锁是不同的锁,可以异步执行,不需要同步

  • 原因
    因为synchronized(this)在代码块和方法上锁的是类的对象,如果多个线程同时争抢锁会导致效率低下。如果此时不锁住类的对象而是锁某一个局部对象,其与需要对象锁的方法或代码块是异步的,大大提高运行效率
    image.png

synchronized锁非this对象时结论

synchronized(x){}此x非this对象时

  • 其他线程执行该代码块时呈同步效果
  • 执行x对象中的synchronized同步方法时呈同步效果
  • 执行x对象中的synchronized(this){}代码块时呈同步效果

synchronized锁static方法和Class

//synchronized锁class和static
class F {
    synchronized static void a() {
        System.out.println("A1");
        try {
            Thread.sleep(1000);
        } catch (InterruptedException e) {
            e.printStackTrace();
        }
        System.out.println("A2");
    }
    void b() {
        synchronized (F.class) {
            System.out.println("B1");
            try {
                Thread.sleep(1000);
            } catch (InterruptedException e) {
                e.printStackTrace();
            }
            System.out.println("B2");
        }
    }
    synchronized  void c() {
        System.out.println("C1");
        try {
            Thread.sleep(1000);
        } catch (InterruptedException e) {
            e.printStackTrace();
        }
        System.out.println("C2");
    }
    synchronized static void d() {
        System.out.println("D1");
        try {
            Thread.sleep(1000);
        } catch (InterruptedException e) {
            e.printStackTrace();
        }
        System.out.println("D2");
    }
    public static void main(String[] args) {
        new Thread(() -> F.a()).start();
        new Thread(() -> F.a()).start();
        new Thread(() -> new F().b()).start();
        new Thread(() -> new F().c()).start();
    }
}

image.png
上图演示的锁住F.class对象和static方法和普通的方法锁时可以看出static和Class对象锁的都是类,因为Class是单例的,且static是和类绑定在一起的所以就算调用实例对象来调用static方法也是并行的

String常量池直接引用内存地址

//直接引用
class G {
    private Integer a = 0;

    void setA(int a) {
        synchronized (this.a) {
            this.a = a;
        }
    }

    int getA() {
        return this.a;
    }

    public static void main(String[] args) {
        G g = new G();
        new Thread(() -> {
            System.out.println(g.getA() + Thread.currentThread().getName());
            g.setA(2);
            try {
                Thread.sleep(2000);
            } catch (InterruptedException e) {
                e.printStackTrace();
            }
            System.out.println(g.getA() + Thread.currentThread().getName());
        }).start();
        new Thread(() -> {
            System.out.println(g.getA() + Thread.currentThread().getName());
            g.setA(1);
            try {
                Thread.sleep(2000);
            } catch (InterruptedException e) {
                e.printStackTrace();
            }
            System.out.println(g.getA() + Thread.currentThread().getName());
        }).start();
    }

}

上述代码的结果如下,因为锁的对象是this.a而且没有用到a所以失效
image.png
但是如果是锁入参数String类型常量池、或者Integer类型由于自动拆装箱缘故,所以直接需要锁等待

class G {
    private String a ="";

    void setA(Integer a) {
        synchronized (a) {
            System.out.println( Thread.currentThread().getName());
            try {
                Thread.sleep(2000);
            } catch (InterruptedException e) {
                e.printStackTrace();
            }
            System.out.println(Thread.currentThread().getName());
        }
    }

    String getA() {
        return this.a;
    }

    public static void main(String[] args) {
        G g = new G();
        new Thread(() -> {
            g.setA(2);
        }).start();
        new Thread(() -> {
            g.setA(2);
        }).start();
    }
}

image.png
此时通过关键字new生成新的内存地址,那么就判定2个对象不同,锁不生效
image.png

synchronized锁无限等待解决方案

  • 背景
    当synchronized在方法上,即锁this时间过长,其他synchronized方法无法调用
  • 方案
    可以使用synchronized锁住方法入参或者this.x的其他对象
    在方法体内调用synchronized(x){}代码块

多线程死锁

不同的线程在相互等待不被释放的对象锁,导致所有的任务都无法继续完成。
死锁时必须避免的,会造成程序假死,是程序设计的BUG,必须要避免双方互相持有对方的锁

//死锁
class H {
    private Object object1=new Object();
    private Object object2=new Object();

    public void a() {
        synchronized (object1) {
            System.out.println(object1);
            try {
                Thread.sleep(200);
            } catch (InterruptedException e) {
                e.printStackTrace();
            }
            synchronized (object2) {
                System.out.println(object2);
            }
        }
    }

    public void b()  {
        synchronized (object2) {
            System.out.println(object2);
            try {
                Thread.sleep(200);
            } catch (InterruptedException e) {
                e.printStackTrace();
            }
            synchronized (object1) {
                System.out.println(object2);
            }
        }
    }
    public static void main(String[] args) {
        H h = new H();
        new Thread(()->h.a()).start();
        new Thread(()->h.b()).start();
    }
}

如下图所示打印2个Object内存地址后就一直等待
image.png
可使用jps、jstack查看死锁情况
image.png
image.png

内部类和静态内部类的synchronized锁

和普通类使用点相同,不做解释

synchronized锁对象改变导致异步执行

synchronized(x)在锁代码块内部改变时,此时如果有其他线程访问的synchronized代码块时因为x已经改变了,如果其内存指向改变的话,那么锁的对象是不同的也就是说可以异步并发执行,而不是同步执行。注意如果是基本类型是没有引用的,直接在方法区和常量池中

synchronized锁对象不改变。

synchronized(x){}代码块在不同的Class类中定义,但在执行多线程时,只要入参x是同一个,那么还是会锁住同步串行执行

synchronized大致使用锁对象总结

class I {
    synchronized public static void a() {
    }
    public void b() {
        synchronized (I.class) {
        }
    }
    synchronized public void c() {
    }
    public void d() {
        synchronized (this) {
        }
    }
    public void e() {
        synchronized ("abc") {
        }
    }
}

在上述中:

  • a和b锁的是同一个I.Class,其同步,与其他异步
  • c和d锁的是同一个I对象,其同步,与其他异步
  • d锁的是字符串“abc”,与其他异步

volatile关键字

特性:

  • 可见性
    B线程能看到A线程更改后的数据
  • 原子性
    1.32位系统中,未使用volatitle声明的数据类型没有原子性
    2.X86的64位JDK中,写double、long是原子性的
    3.volatile声明的int类型的变量i进行i++操作也是非原子的
  • 禁止冲排序

可见性

Java多线程运行时,对象存在于私有队线程中

  • 加入volatile关键字后当线程访问该变量时,强制从公共堆栈中进行取值
  • synchronized代码块也具有增加可见性的作用

原子性

  • volatile修饰变量并不能让其操作变为原子性,只能增加其可见性
  • 使用Atomic原子类可以实现i++的原子性效果

禁止重排序

  • 在Java程序运行时,JIT(Just-In-Time Complier,即时编译器)可以动态的改变程序代码运行的顺序
A代码-重耗时
B代码-轻耗时
C代码-重耗时
D代码-轻耗时

JIT可能加工如下

B代码-轻耗时
D代码-轻耗时
A代码-重耗时
C代码-重耗时
  • 加入了volatile关键字之后则可以限制冲排序
A变量的操作
B变量的操作
volatile z变量的操作
C变量的操作
D变量的操作

此时变量z是一个屏障,AB和CD不可能跨越z重新排序

  • synchronized也具有该特性

总结

synchronized

保证同一时刻,只有一个线程可以执行某一个方法,或代码块,synchronized可以修饰方法或代码块

  • 可见性
  • 原子性
    实现了同步就实现了原子性
  • 禁止重排序

volatile

让其他线程可以看到最新的值,只能修饰变量

  • 可见性
  • 原子性
    1.32位系统中未声明未volatile的数据类型没有实现原子性,如果想实现则添加volatile
    2.64位操作系统是根据具体架构实现的。声明未volatile也不一定具有原子性
    3.对volatile声明的变量i进行i++是不具备原子性的
  • 禁止重排序

使用场景

1.想实现一个变量的值被更改时,其他线程可以获取到最新的值时,可以对变量使用volatile
2.多个线程对同一个对象中的一个实例变量进行操作时,为了避免线程安全问题可以使用synchronized关键字


这个家伙很懒,啥也没有留下😋