synchronized

方法内变量是线程安全

关于“线程安全”和“非线程安全”相关的技术点,是学习多线程一定会遇到的经典问题。“非线程安全”通常发生在多个线程对同一个对象的实例变量进行并发访问。产生的后果就是“脏读”,也就是读取到的数据其实是被其他线程更改过的。synchronized 通过同步互斥来解决这个问题。

注意,“非线程安全”问题存在于“实例变量”中,如果是方法内部的私有变量。那么不存在“非线程安全”问题。所得到的结果自然就是“线程安全”的。

package com.skystep.thread.threadsafe;

public class HasSelfPrivateNum {

    public void addI(String username) {
        try{
            int num = 0;
            if (username.equals("a")) {
                num = 100;
                System.out.println("a set over");
                Thread.sleep(2000);
            }else {
                num = 200;
                System.out.println("b set over");
            }
            System.out.println(username + " num=" + num);
        } catch (InterruptedException e) {
            e.printStackTrace();
        }

    }

}

package com.skystep.thread.threadsafe;

public class ThreadA extends Thread{
    private HasSelfPrivateNum numRef;

    public ThreadA(HasSelfPrivateNum numRef) {
        this.numRef = numRef;
    }


    @Override
    public void run() {
        super.run();
        this.numRef.addI("a");
    }
}
package com.skystep.thread.threadsafe;

public class ThreadB extends Thread{
    private HasSelfPrivateNum numRef;

    public ThreadB(HasSelfPrivateNum numRef) {
        this.numRef = numRef;
    }


    @Override
    public void run() {
        super.run();
        this.numRef.addI("b");
    }
}
package com.skystep.thread.threadsafe;

public class Run {
    public static void main(String[] args) {
        HasSelfPrivateNum hasSelfPrivateNum = new HasSelfPrivateNum();
        ThreadA a = new ThreadA(hasSelfPrivateNum);
        a.start();
        ThreadB b = new ThreadB(hasSelfPrivateNum);
        b.start();
    }
}
执行结果:
a set over
b set over
b num=200
a num=100

其中 num 为方法内变量,两个线程如期望输出结果。可见,方法中的变量不存在非线程安全问题,永远是线程安全的,因为方法内部的变量是私有造成的。

实例变量是非线程安全

如果多个线程并发访问同一个对象的某个实例变量,便可能发生“非线程安全”问题,线程访问的对象如果有多个实例变量,则运行结果有可能出现交叉的情况。如果对象只有一个实例变量,则有可能出现覆盖的情况。

package com.skystep.thread.threadunsafe;

public class HasSelfPrivateNum {
	private int num = 0;
    public void addI(String username) {
        try{
            if (username.equals("a")) {
                num = 100;
                System.out.println("a set over");
                Thread.sleep(2000);
            }else {
                num = 200;
                System.out.println("b set over");
            }
            System.out.println(username + " num=" + num);
        } catch (InterruptedException e) {
            e.printStackTrace();
        }

    }

}

package com.skystep.thread.threadunsafe;

public class ThreadA extends Thread{
    private HasSelfPrivateNum numRef;

    public ThreadA(HasSelfPrivateNum numRef) {
        this.numRef = numRef;
    }


    @Override
    public void run() {
        super.run();
        this.numRef.addI("a");
    }
}
package com.skystep.thread.threadunsafe;

public class ThreadB extends Thread{
    private HasSelfPrivateNum numRef;

    public ThreadB(HasSelfPrivateNum numRef) {
        this.numRef = numRef;
    }


    @Override
    public void run() {
        super.run();
        this.numRef.addI("b");
    }
}
package com.skystep.thread.threadunsafe;

public class Run {
    public static void main(String[] args) {
        HasSelfPrivateNum hasSelfPrivateNum = new HasSelfPrivateNum();
        ThreadA a = new ThreadA(hasSelfPrivateNum);
        a.start();
        ThreadB b = new ThreadB(hasSelfPrivateNum);
        b.start();
    }
}
执行结果:
a set over
b set over
b num=200
a num=200

两个线程同时访问对象中实例变量。便出现了“非线程安全问题”。对于此类问题,早在 JAVA 1.6时便提供了 synchronized 同步互斥锁来解决此问题。

synchronized 同步方法

在 Java 语言中,每一个对象都会内置一把锁(监视器锁),这是一种互斥锁,每一时间片只能有一个线程获取该锁,其他线程进入等待,直到原先线程释放锁。 线程可以使用 synchronized 关键字来获取对象上的锁。synchronized 关键字可应用在方法级别(粗粒度锁)或者是代码块级别(细粒度锁)。

package com.skystep.thread.sync;

public class HasSelfPrivateNum {
    private int num = 0;

    public synchronized void addI(String username) {
        try {
            if (username.equals("a")) {
                num = 100;
                System.out.println("a set over");
                Thread.sleep(2000);
            } else {
                num = 200;
                System.out.println("b set over");
            }
            System.out.println(username + " num=" + num);
        } catch (InterruptedException e) {
            e.printStackTrace();
        }

    }

}
package com.skystep.thread.sync;

public class ThreadA extends Thread{
    private HasSelfPrivateNum numRef;

    public ThreadA(HasSelfPrivateNum numRef) {
        this.numRef = numRef;
    }


    @Override
    public void run() {
        super.run();
        this.numRef.addI("a");
    }
}

package com.skystep.thread.sync;

public class ThreadB extends Thread{
    private HasSelfPrivateNum numRef;

    public ThreadB(HasSelfPrivateNum numRef) {
        this.numRef = numRef;
    }


    @Override
    public void run() {
        super.run();
        this.numRef.addI("b");
    }
}
package com.skystep.thread.sync;

public class Run {
    public static void main(String[] args) {
        HasSelfPrivateNum hasSelfPrivateNum = new HasSelfPrivateNum();
        ThreadA a = new ThreadA(hasSelfPrivateNum);
        a.start();
        ThreadB b = new ThreadB(hasSelfPrivateNum);
        b.start();
    }
}
执行结果:
a set over
a num=100
b set over
b num=200

通过对 addI 方法添加 synchronized 关键字修饰之后,该程序重新符合预期输出结果。当 ThreadA 线程获取到 hasSelfPrivateNum 对象锁时,ThreadB 会进入等待,直到 ThreadA 执行完 addI 方法。这就是同步互斥。

synchronized 错误使用 多个对象多个锁

从前面例子来看,关键字 synchronized 取得的锁都是对象锁,而不是一段代码或者函数,不同线程必须竞争同一把锁,才能达到同步互斥的目的。但是对于初学者常常会出现使用错误。出现多个对象多把锁的问题。

package com.skystep.thread.syncerroruse;

public class HasSelfPrivateNum {
    private int num = 0;

    public synchronized void addI(String username) {
        try {
            if (username.equals("a")) {
                num = 100;
                System.out.println("a set over");
                Thread.sleep(2000);
            } else {
                num = 200;
                System.out.println("b set over");
            }
            System.out.println(username + " num=" + num);
        } catch (InterruptedException e) {
            e.printStackTrace();
        }

    }

}
package com.skystep.thread.syncerroruse;

public class ThreadA extends Thread{
    private HasSelfPrivateNum numRef;

    public ThreadA(HasSelfPrivateNum numRef) {
        this.numRef = numRef;
    }


    @Override
    public void run() {
        super.run();
        this.numRef.addI("a");
    }
}

package com.skystep.thread.syncerroruse;

public class ThreadB extends Thread{
    private HasSelfPrivateNum numRef;

    public ThreadB(HasSelfPrivateNum numRef) {
        this.numRef = numRef;
    }


    @Override
    public void run() {
        super.run();
        this.numRef.addI("b");
    }
}
package com.skystep.thread.syncerroruse;

public class Run {
    public static void main(String[] args) {
        HasSelfPrivateNum hasSelfPrivateNumA = new HasSelfPrivateNum();
        ThreadA a = new ThreadA(hasSelfPrivateNumA);
        a.start();
        HasSelfPrivateNum hasSelfPrivateNumB = new HasSelfPrivateNum();
        ThreadB b = new ThreadB(hasSelfPrivateNumB);
        b.start();
    }
}
执行结果:
a set over
b set over
b num=200
a num=100

从例子来看,main 线程创建了多个线程。ThreadA 线程使用的是 hasSelfPrivateNumA 对象作为锁,ThreadB 使用 hasSelfPrivateNumB 对象锁,并不能达到同步互斥的目标。

synchronized 方法和锁对象 锁住的是谁

使用 synchronized 修饰函数,便是将函数调用者实例作为对象锁。不同线程调用被 synchronized 修饰的函数,必须抢占该对象锁,谁先抢到谁先执行函数代码,抢不到的线程进入阻塞状态,直到锁被释放。

package com.skystep.thread.syncerroruse;

public class Run {
    public static void main(String[] args) {
        HasSelfPrivateNum hasSelfPrivateNumA = new HasSelfPrivateNum();
        ThreadA a = new ThreadA(hasSelfPrivateNumA);
        a.start();
        HasSelfPrivateNum hasSelfPrivateNumB = new HasSelfPrivateNum();
        ThreadB b = new ThreadB(hasSelfPrivateNumB);
        b.start();
    }
}
执行结果:
a set over
b set over
b num=200
a num=100

ThreadA 执行时 执行函数 run 调用了 hasSelfPrivateNumA对象的 addI 方法,抢占锁对象是 hasSelfPrivateNumA,而 ThreadB 执行时 执行函数 run 调用了 hasSelfPrivateNumB对象的 addI 方法,抢占的锁是 hasSelfPrivateNumB 。

synchronized 锁重入 无限套娃不违法

关键字 synchronized 拥有重入锁的功能 。当一个线程获取到对象锁时,再次请求此对象锁时是可以再次得到该对象的锁的。JAVA 中称为锁重入。是同一锁,才有重入一说,套的娃必须是同一个。

重入锁的概念是:自己可以再次获取自己的内部锁,比如线程 A 获得某个对象的锁,此时锁没有释放,当其再次获取这个对象的锁的时候可以获得。如果不可的重入的锁,就会造成死锁。

synchronized 锁释放 遇异常自动释放

ThreadA 执行时 执行函数 run 调用了 hasSelfPrivateNumA对象的 addI 方法,抢占锁对象是 hasSelfPrivateNumA,而 ThreadB 执行时 执行函数 run 调用了 hasSelfPrivateNumB对象的 addI 方法,抢占的锁是 hasSelfPrivateNumB 。

synchronized 同步代码块

被 synchronized 关键字修饰的语句块会自动被加上内置锁,从而实现同步。

public class Counter {
    private int num;

    public int getNum() {
        return num;
    }

    public void setNum(int num) {
        this.num = num;
    }

    public void add() {
    	synchronized (this){
    		this.num++;
		}
    }
}
public class Sync {
    public static void main(String[] args) {
        Counter counter = new Counter();
        counter.setNum(0);

        Thread[] threds = new Thread[2];
        for (int i = 0; i < 2; i++) {
            threds[i] = new Thread(new Runnable() {
                @Override
                public void run() {
                    for (int j = 0; j < 2000; j++) {
                        counter.add();
                    }
                }
            });
        }

        threds[0].start();
        threds[1].start();

        try {
            threds[0].join();
            threds[1].join();
        } catch (InterruptedException e) {
            e.printStackTrace();
        }

        System.out.println("num:" + counter.getNum());

    }
}

执行结果:
num:4000

在代码段 num++ 中添加 synchronized 修饰,便完成共有变量 num 的读写同步。

简述 synchronized 实现原理

在多线程并发编程中 synchronized 一直是元老级的存在。很多人称之为重量级锁,但在 JAVA SE1.6 进行了各种优化之后,已经没有那么重量级了。为了减少获取锁和释放锁带来的消耗而引进了偏向锁和轻量级锁。

JAVA 中的每一个对象都可以作为锁,具体表象有如下三种:

  • 对于普通同步方法,锁是当前的实例对象;
  • 对于静态同步方法,锁是当前类的 Class 对象;
  • 对于同步方法快,锁的是括号里配置的对象;

在 JVM 规范中可以知道,JVM 基于进入和退出 Monitor 对象来实现方法同步和代码块同步。monitorenter 指令在编译后插入到同步代码块的开始位置,而 monitorexit 是插入到方法结束处或者异常的地方,JVM 保证了每个 monitorenter 都有与之对应的 monitorexit。任何对象都有一个 monitor 与之关联,当且一个 monitor 被持有之后,它将处于锁定状态。线程执行到 monitorexit 指令后,将会尝试获取对象对应的 monitor 的所有权,即尝试获得对象的锁。

posted @ 2021-12-01 19:17  yaomianwei  阅读(5)  评论(0编辑  收藏  举报