4. synchronized关键字(一)

一、线程安全和不安全

  • 非线程安全:在多个线程对同一个对象的实例变量进行并发访问时会出现值被更改、值不同步的情况
  • 线程安全:获得的实例变量的值是经过同步处理的,按照顺序执行,不会出现脏读情况

举个例子:5个销售员, 卖同一堆货物,每个销售员在卖出一个货品后,并不是立即知道当前货物剩余数量的,因为在他卖出的同时,可能其他销售员已经卖出好几个货品了,如果这个时候就减1,那么就会产生数据的不同步,可能售货员1计算剩余20个,售货员2计算剩余25个。因此,需要使5个卖货品的过程进行同步,即按顺序的方式进行减1,也就是每卖一次货物,要在当前剩余的数量上减1,即售货员1计算剩余20个,此时售货员2卖出一个,计算剩余数量20-1=19个

先看个线程不安全的例子

public class MyThread6_1 implements Runnable {

    private int count = 5;

    @Override
    public void run() {
        count--;
        System.out.println("由 " + Thread.currentThread().getName() +
                " 计算, count=" + count);
    }

    public static void main(String[] args) {
        MyThread6_1 thread = new MyThread6_1();
		// 5个线程共同调用线程 Thread6_1,同时共同修改变量 count
        Thread t1 = new Thread(thread);
        Thread t2 = new Thread(thread);
        Thread t3 = new Thread(thread);
        Thread t4 = new Thread(thread);
        Thread t5 = new Thread(thread);

        t1.start();
        t2.start();
        t3.start();
        t4.start();
        t5.start();
    }
}

结果是:

由 Thread-0 计算, count=3
由 Thread-2 计算, count=2
由 Thread-1 计算, count=3
由 Thread-4 计算, count=0
由 Thread-3 计算, count=1

可以看到,Thread-0Thread-1 同时打印的 count 都是3,说明两个线程是一起执行 run 方法的,即同时减1,会产生非线程安全的问题,而我们理想的结果是,每个结果都是依次递减的

我们再来看一个线程安全的例子,在 run 方法的前面加上 synchronized 关键字

public class MyThread6_1 implements Runnable {

    private int count = 5;

    //在 run 方法前面加上 synchronized 关键字
    @Override
    public synchronized void run() {
        count--;
        System.out.println("由 " + Thread.currentThread().getName() +
                " 计算, count=" + count);
    }

    public static void main(String[] args) {
        MyThread6_1 thread = new MyThread6_1();
		// 5个线程共同调用线程 Thread6_1,同时共同修改变量 count
        Thread t1 = new Thread(thread);
        Thread t2 = new Thread(thread);
        Thread t3 = new Thread(thread);
        Thread t4 = new Thread(thread);
        Thread t5 = new Thread(thread);

        t1.start();
        t2.start();
        t3.start();
        t4.start();
        t5.start();
    }
}

结果是:

由 Thread-0 计算, count=4
由 Thread-1 计算, count=3
由 Thread-3 计算, count=2
由 Thread-2 计算, count=1
由 Thread-4 计算, count=0

可以看到,此时的结果没有在出现相同的 count 值的问题了。

当我们在方法前面加上 synchronized 关键字的时候,使得多个线程在执行 run 方法的时候,以排队的方式进行处理。当一个线程在调用 run 方法前,会先判断 run 方法有没有被上锁,如果上锁,说明有其他线程正在调用 run 方法,必须等待其他线程对 run 方法调用结束后才可以执行 run 方法。这样就实现了排队调用 run 方法的目的,即按顺序完成对 count 变量减 1 的操作。

synchronized 可以在任意对象和方法上加锁,加索的这段代码称为 互斥区 或者 临界区,当一个线程想要执行同步方法里面的代码时,线程首先要尝试去拿到这把锁,如果能够拿到这把锁,那么这个线程就可以执行 synchronized 里面的代码,如果不能拿到这把锁,那么这个线程就会不断尝试去拿这把锁,直到拿到为止。

二、synchronized同步方法

2.1 方法内的变量为线程安全

非线程安全存在于实例变量中的,如果是方法内部的私有变量,则不会出现非线程安全的问题,看个例子吧

HashSelPrivateNum 类

class HashSelPrivateNum {

    public void addI(String username) throws InterruptedException {
        //该变量是 addI 方法内部的私有变量
        int num;
        
        if (username.equals("thread a")) {
            num = 100;
            System.out.println("a set over" + " " + System.currentTimeMillis());
            //使当前线程休眠
            Thread.sleep(2000);
        } else {
            num = 200;
            System.out.println("b set over" + " " + System.currentTimeMillis());
        }
        System.out.println(username + " num = " + num + " " + System.currentTimeMillis());
    }
}

创建线程 ThreadB

//创建线程类 ThreadB
class ThreadB extends Thread {
    private HashSelPrivateNum numRef;

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

    @Override
    public void run() {
        try {
            numRef.addI("thread b");
        } catch (InterruptedException e) {
            e.printStackTrace();
        }
    }
}

//创建线程类 ThreadA
public class ThreadA extends Thread {

    private HashSelPrivateNum numRef;

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

    @Override
    public void run() {
        try {
            numRef.addI("thread a");
        } catch (InterruptedException e) {
            e.printStackTrace();
        }
    }

    /**
     * 此时多个对象访问同一个对象
     *
     * 线程 Thread A 下先执行了带 synchronized 关键字的方法 addI(),此时线程 Thread A 就持有了对象 num 的锁
     * 此时线程 Thread B 只能等待线程 A 执行完了之后才能执行线程 B 中的 run() 方法,因此此时方法是同步的
     */
    public static void main(String[] args) {
        HashSelPrivateNum num = new HashSelPrivateNum();
        ThreadA threadA = new ThreadA(num);
        threadA.start();

        ThreadB threadB = new ThreadB(num);
        threadB.start();
    }
}

结果是:

a set over 1540024730771
b set over 1540024730771
thread b num = 200 1540024730772
thread a num = 100 1540024732772

可以看到,此时虽然类 HashSelPrivateNum 的方法 addI 不是同步方法,且不是按照代码的顺序输出的,但是当线程 A 和线程 B 调用的时候,依然是线程安全的,即不会造成数值的覆盖或者重复等,这是方法 addI 内部的变量 num 是私有的这个性质造成的

2.2 实例变量为非线程安全

如果多个线程同时访问1个对象中的实例变量,则会出现非线程安全

还是上面的方法,我们将 HashSelPrivateNum 类的 num 变量变为实例变量

class HashSelPrivateNum {

    //如果变量不是方法的私有变量,此时变成了公共变量
    private int num = 0;

    public void addI(String username) throws InterruptedException {
        if (username.equals("thread a")) {
            num = 100;
            System.out.println("a set over" + " " + System.currentTimeMillis());
            //使当前线程休眠
            Thread.sleep(2000);
        } else {
            num = 200;
            System.out.println("b set over" + " " + System.currentTimeMillis());
        }
        System.out.println(username + " num = " + num + " " + System.currentTimeMillis());
    }
}

线程 A 和线程 B 的方法和上面一样,结果如下:

a set over 1540024616286
b set over 1540024616286
thread b num = 200 1540024616286
thread a num = 200 1540024618287

此时变量不再是私有变量了,从输出可以看到,除了顺序不是按照代码顺序之外,此时的 num 变量也出现了重复的情况,即线程 A 和线程 B 同时对实例变量 num 进行了输出,因此出现了 非线程安全 的问题,对于这个问题,我们只需要在 addI 方法前面添加 synchronized 关键字即可

class HashSelPrivateNum {

    //如果变量不是方法的私有变量,此时变成了公共变量
    private int num = 0;

    //在 addI 方法前面添加 synchronized 关键字
    public synchronized void addI(String username) throws InterruptedException {
        if (username.equals("thread a")) {
            num = 100;
            System.out.println("a set over" + " " + System.currentTimeMillis());
            //使当前线程休眠
            Thread.sleep(2000);
        } else {
            num = 200;
            System.out.println("b set over" + " " + System.currentTimeMillis());
        }
        System.out.println(username + " num = " + num + " " + System.currentTimeMillis());
    }
}

线程 A 和线程 B 的代码同上,main 方法也同上,结果是:

a set over 1540025424653
thread a num = 100 1540025426655
b set over 1540025426655
thread b num = 200 1540025426655

可以看到,addI 是同步方法,不仅顺序输出顺序严格遵循代码执行顺序,数值也是各自线程的数值,因此,是 线程安全

得出结论:在两个线程访问同一个对象中的同步方法时一定是线程安全的

2.3 多个对象多个锁

创建 HashSelPrivateNum 类,其中的方法 addI 是同步方法

class HashSelPrivateNum {

    //如果变量不是方法的私有变量,此时变成了公共变量,则有可能出现线程安全问题,此时需要加 synchronized 关键字
    private int num = 0;

    public synchronized void addI(String username) throws InterruptedException {
        //该变量是 addI 方法内部的私有变量,此时不加 synchronized 关键字也不会存在线程安全问题
//        int num;

        if (username.equals("thread a")) {
            num = 100;
            System.out.println("a set over" + " " + System.currentTimeMillis());
            //使当前线程休眠
            Thread.sleep(2000);
        } else {
            num = 200;
            System.out.println("b set over" + " " + System.currentTimeMillis());
        }
        System.out.println(username + " num = " + num + " " + System.currentTimeMillis());
    }
}

创建线程 A1 和线程 B1,同时使用这2个线程访问2个不同的对象

class ThreadB1 extends Thread {

    private HashSelPrivateNum numRef;

    public ThreadB1(HashSelPrivateNum numRef) {
        this.numRef = numRef;
    }

    @Override
    public void run() {
        try {
            numRef.addI("thread b");
        } catch (InterruptedException e) {
            e.printStackTrace();
        }
    }
}

public class ThreadA1 extends Thread {

    private HashSelPrivateNum numRef;

    public ThreadA1(HashSelPrivateNum numRef) {
        this.numRef = numRef;
    }

    @Override
    public void run() {
        try {
            numRef.addI("thread a");
        } catch (InterruptedException e) {
            e.printStackTrace();
        }
    }

    /**
     * 多个线程访问多个对象,该实例创建了 2 个 HashSelPrivateNum 类的对象,就产生了 2 个对象锁
     *  当线程 Thread A1 执行 synchronized 方法 addI(),便持有该方法所属对象 numRef1 的锁
     *  此时线程 B 并不用等待,而是持有对象 numRef2 的锁。此时方法是异步执行的
     */
    public static void main(String[] args) {
        //创建两个对象
        HashSelPrivateNum numRef1 = new HashSelPrivateNum();
        HashSelPrivateNum numRef2 = new HashSelPrivateNum();
		//两个线程访问两个不同的对象
        ThreadA1 threadA1 = new ThreadA1(numRef1);
        threadA1.start();

        ThreadB1 threadB1 = new ThreadB1(numRef2);
        threadB1.start();
    }
}

结果是:

a set over 1540027131988
b set over 1540027131988
thread b num = 200 1540027131989
thread a num = 100 1540027133989

可以看到,当用两个线程分别访问同一个类的不同实例的时候,虽然类中方法是同步方法,但是输出的结果是 异步 的,即不是按照正确的顺序来执行的

关键字 synchronized 取得的锁都是对象锁,而不是把一段代码或者方法当作锁,之前的那个例子,是 多个线程访问同一个对象,此时哪个线程先执行带有 synchronized 关键字的方法,哪个线程就持有了该方法所属对象的锁,此时其他线程只能等待,等待这个持有对象锁的线程执行完 run 方法之后,在继续执行。因此这个执行和等待的过程是严格按照顺序,即 同步 的方式来进行的

该实例创建了 2 个 HashSelPrivateNum 类的对象,就产生了 2 个对象锁,虽然有锁,但都是各自的,即自己执行自己的,不需要等待其他线程完成才能执行,也不会产生数据的重复

2.4 脏读

即两个线程对同一个对象中的数据进行修改时发生的数据交叉或者重复的问题

public class PubVar {

    public String username = "AAA";
    public String password = "123456";

    synchronized public void setValue(String username, String password) {
        try {
            this.username = username;
            System.out.println(Thread.currentThread().getName() + " setValue begin"
                    + " user = " + this.username + " pas = " + this.password + " " + System.currentTimeMillis());
            //在给 password 赋值的时候将当前线程睡眠 2s
            Thread.sleep(2000);
            this.password = password;
            System.out.println(Thread.currentThread().getName() + " setValue end"
                    + " user = " + this.username + " pas = " + this.password + " " + System.currentTimeMillis());
        } catch (InterruptedException e) {
            e.printStackTrace();
        }
    }

    public void getValue() {
        System.out.println(Thread.currentThread().getName() + " getvalue"
                + " user = " + username + " pas = " + password + " " + System.currentTimeMillis());
    }

}

创建线程 ThreadA4

public class ThreadA4 extends Thread {

    private PubVar pubVar;

    public ThreadA4(PubVar pubVar) {
        this.pubVar = pubVar;
    }

    @Override
    public void run() {
        pubVar.setValue("BBB","654321");
    }

    public static void main(String[] args) throws InterruptedException {
        PubVar pubVar = new PubVar();
        ThreadA4 threadA4 = new ThreadA4(pubVar);
        threadA4.start();
        //使得 main 线程睡眠 2s
        Thread.sleep(2000);
        pubVar.getValue();
    }
}

结果是:

Thread-0 setValue begin user = BBB pas = 123456 1540030412129
main getvalue user = BBB pas = 123456 1540030414129		//该数据的 pas 是还没有赋值之前的值
Thread-0 setValue end user = BBB pas = 654321 1540030414129

因为 PubVar 类中的 getValue 方法并不是同步的,索引可以在任意时刻调用,因此, setValue 方法还没有给 password 变量赋完值,就直接执行 getValue 方法了,此时输出的数据是只有一半是正确的

如果我们给 getValue 方法加上 synchronized 关键字,这个时候 setValue 和 getValue 就依次执行了,结果是:

Thread-0 setValue begin user = BBB pas = 123456 1540040547078
Thread-0 setValue end user = BBB pas = 654321 1540040549080
main getvalue user = BBB pas = 654321 1540040549080

分析一下两个过程,首先,线程 ThreadA4 获得了对象 pubVar 的锁,然后在线程 ThreadA4 中执行对象所在类的同步方法 setValue,这个时候,其他线程只有等线程 ThreadA4 执行完 setValue 后才能执行这个方法

  • 对于第 1 个例子,其中的 getValue 不是同步方法,同时也是线程 main 调用的,这个时候,因为不是同步方法,所以线程 main 可以在任意时刻调用这个非同步方法(没有 synchronized 修饰),也就是说,可能向上面一样,赋值到一半就输出了,也有可能全部赋值完再输出,只是这个时候和对象锁无关了
  • 对于第 2 个例子,其中 getValue 是同步方法,此时类 PubVar 中就有两个同步方法,因为 setValue 正在执行,即线程 ThreadA4 持有该方法所在对象 pubVar 的对象锁,而线程 main 也要调用该对象的另一个同步方法 getValue,所以线程 main 必须等待线程 ThreadA4 执行完 setValue 方法并且释放对象锁之后才能调用 getValue 方法。这时线程 ThreadA4 已经按照代码执行顺序对变量 username 和 password 进行了赋值,最好线程 main 再调用方法进行输出,这个时候不存在脏读的情况

简单来说:

  1. 一个对象里面,如果只有一个同步方法 X,如果它被一个线程 A 调用,即线程 A 获取了 X 所在对象的锁,那么其他线程必须等到线程 A 执行完方法 X 之后才能调用方法 X,但是其他线程却可以随意调用对象里的非同步方法,与对象锁无关,这个时候会出现
  2. 一个对象里面,如果有一个同步方法 X,它被一个线程 A 调用,即线程 A 获取了 X 所在对象的锁,同时还有一个同步方法 Y,它被另一个线程 B 调用,此时线程 B 不能随意执行方法 Y 了,必须等到线程 A 将方法 X 执行完,释放对象锁之后才能执行方法 Y,这个时候与对象锁有关,不会出现脏读

2.5 锁重入

关键字 synchronized 具有锁重入的功能,即,当一个线程得到一个对象锁之后,再次请求次对象锁是可以再次得到该对象的锁的。因此,可以在一个 synchronized 方法内部调用本类的其他 synchronized 方法,这个时候用于可以得到内部锁

class Service {

    synchronized public void service1() {
        System.out.println(Thread.currentThread().getName() + " service");
        //调用同步方法 service2()
        service2();
    }

    synchronized public void service2() {
        System.out.println(Thread.currentThread().getName() + " service2");
        //调用同步方法 service3()
        service3();
    }

    synchronized public void service3() {
        System.out.println(Thread.currentThread().getName() + " service3");
    }

}

public class ThreadA5 extends Thread {

    @Override
    public void run() {
        //线程 ThreadA5 得到了 service 对象锁
        Service service = new Service();
        //通过 service 对象调用同步方法 service1()
        service.service1();
    }

    public static void main(String[] args) {
        ThreadA5 threadA5 = new ThreadA5();
        threadA5.start();
    }

}

结果是:

Thread-0 service
Thread-0 service2
Thread-0 service3

可重入锁:自己可以再次获取自己的内部锁,如果有 1 个线程获得个某个对象的锁,此时这个对象锁还没有被释放,当这个线程想要再次获取这个对象锁的时候还是可以获取的,如果有不可锁重入的话,会造成死锁

可重入锁也可以用于继承关系中,即子类可以调用父类的 synchronized 方法

但是得注意的是:同步不具有继承性。看个例子:

class Main2 {
	//父类的 serviceMethod() 是同步方法
    synchronized public void serviceMethod() {
        try {
            System.out.println("int main 下一步 sleep begin "
                    + Thread.currentThread().getName() + " time = "
                    + System.currentTimeMillis());
            Thread.sleep(3000);
            System.out.println("int main 下一步 sleep end "
                    + Thread.currentThread().getName() + " time = "
                    + System.currentTimeMillis());
        } catch (InterruptedException e) {
            e.printStackTrace();
        }
    }
}

class Sub2 extends Main2 {
	
    //子类继承父类的同步方法
    @Override
    public void serviceMethod() {
        try {
            System.out.println("int sub 下一步 sleep begin "
                    + Thread.currentThread().getName() + " time = "
                    + System.currentTimeMillis());
            Thread.sleep(3000);
            System.out.println("int sub 下一步 sleep end "
                    + Thread.currentThread().getName() + " time = "
                    + System.currentTimeMillis());
        } catch (InterruptedException e) {
            e.printStackTrace();
        }
    }
}

class ThreadB8 extends Thread {

    private Sub2 sub2;

    public ThreadB8(Sub2 sub2) {
        this.sub2 = sub2;
    }

    @Override
    public void run() {
        sub2.serviceMethod();
    }
}

public class ThreadA8 extends Thread{

    private Sub2 sub2;

    public ThreadA8(Sub2 sub2) {
        this.sub2 = sub2;
    }

    @Override
    public void run() {
        sub2.serviceMethod();
    }

    public static void main(String[] args) {
        Sub2 sub2 = new Sub2();
        ThreadA8 threadA8 = new ThreadA8(sub2);
        threadA8.setName("AA");
        threadA8.start();

        ThreadB8 threadB8 = new ThreadB8(sub2);
        threadB8.setName("BB");
        threadB8.start();
    }
}

结果是:

int sub 下一步 sleep begin AA time = 1540131922832
int sub 下一步 sleep begin BB time = 1540131922834
int sub 下一步 sleep end AA time = 1540131925834
int sub 下一步 sleep end BB time = 1540131925835

通过结果发现,两个线程调用子类的方法后,不是同步执行的,说明子类继承父类的同步方法并不具有同步性,如果在子类的方法前面加上 synchronized 关键字,此时结果是:

int sub 下一步 sleep begin AA time = 1540132164713
int sub 下一步 sleep end AA time = 1540132167713
int sub 下一步 sleep begin BB time = 1540132167713
int sub 下一步 sleep end BB time = 1540132170714

此时方法同步了,说明同步不能被继承,得在子类的方法中添加 synchronized 关键字才可以得到同步方法

三、参考

《Java多线程编程核心技术》

猜你喜欢

转载自blog.csdn.net/babycan5/article/details/83246808