多线程编程之线程的同步机制(上): Synchronized同步方法

411 阅读11分钟

小知识,大挑战!本文正在参与“   程序员必备小知识   ”创作活动

作者的其他平台:

| CSDN:blog.csdn.net/qq_4115394…

| 掘金:juejin.cn/user/651387…

| 知乎:www.zhihu.com/people/1024…

| GitHub:github.com/JiangXia-10…

| 公众号:1024笔记

本文大概12425字,读完共需30分钟

1 前言

多线程中的同步,指的是如何开发出线程安全的程序或者应用,也就是得解决非线程安全所带来的一些相关问题-----脏读

线程安全非线程安全是学习多线程编程以及日常开发时一定会遇到的问题。非线程安全其实当多个线程访问同一个对象中的成员变量时产生的,产生的后果就是脏读,就是取到的数据其实是被更改过的。而线程安全就是以获取的成员变量的值是经过同步处理的,不会出现脏读的现象。

说到线程同步就必须得说到java中的synchronized关键字。

Synchronized关键字是一种同步锁解决的是多个线程之间访问资源的同步性,synchronized关键字可以保证被它修饰的方法或者代码块在任意时刻只能有一个线程执行。其他线程 必须等待当前线程执行完该方法(代码块)后才能执行该方法 (代码块)。

所以 Synchronized关键字有两个作用:

1、Synchronized同步方法

2、Synchronized同步代码块

这块的内容比较长,所以这里分为两个部分来写。这篇文章就先写Synchronized同步方法

本章内容有:

1、局部变量是线程安全的

2、成员变量不是线程安全的

3、多个对象使用多个对象锁

4、synchronized方法锁的是整个对象

5、脏读

6、锁重入

7、锁的自动释放

8、同步不具有继承性

2 正文

1、局部变量是线程安全的

局部变量不存在非线程安全问题,永远是线程安全的,这是由于局部变量是私有的所造成的。

即线程a修改局部变量,不会影响线程b的访问。比如:

package com.jiangxia.chap2;

/**
 * 线程同步机制
 * author:jiangxia
 * date:2021-04-05
 */
public class Demo1 {
    public static void main(String[] args) {
        Servie1 servie1 = new Servie1();

        Thread t1 = new Thread1(servie1);
        t1.start();

        Thread t2 = new Thread2(servie1);
        t2.start();


    }
}
class Servie1{

    public void set(String string){
        int num = 0;

        if("a".equals(string)){
            num = 100;
            System.out.println("a set");

            try {
                //睡眠的目的是为了等待另一个线程修改num的值
                Thread.sleep(2000);
            } catch (InterruptedException e) {
                e.printStackTrace();
            }
        }else{
            num = 200;
            System.out.println("b set");
        }
        System.out.println("string="+string+";num="+num);

    }
}

class Thread1 extends  Thread{
    private Servie1 servie1;

    public Thread1(Servie1 servie1){
        this.servie1 = servie1;
    }

    @Override
    public void run() {
        servie1.set("a");
    }
}


class Thread2 extends  Thread{
    private Servie1 servie1;

    public Thread2(Servie1 servie1){
        this.servie1 = servie1;
    }

    @Override
    public void run() {
        servie1.set("b");
    }
}
复制代码

图片

2、成员变量不是线程安全的

在java中成员变量不是线程安全的。这里顺便说下什么是成员变量以及局部变量。

局部变量:在方法内定义的变量称为“局部变量”或“临时变量”,方法结束后局部变量占用的内存将被释放。

成员变量:在类体的变量部分中定义的变量。

局部变量和成员变量主要是他们作用域的区别

成员变量是类内部;

局部变量是定义在其方法体内部(或者方法体内部的某一程序块内)。

成员变量可以不显式初始化,它们可以由系统设定默认值;

局部变量没有默认值,所以必须设定初始赋值。

在内存中的位置也不一样。成员变量在所在类被实例化后,存在堆内存中;局部变量在所在方法调用时,存在栈内存空间中。

代码:

package com.jiangxia.chap2;

/**
 * 线程的同步机制 成员变量不是线程安全的
 * author:jiangxia
 */

public class Demo2 {
    public static void main(String[] args) {
        Servie2 s = new Servie2();

        Thread t1 = new ThreadA(s);
        t1.start();

        Thread t2 = new ThreadB(s);
        t2.start();
    }
}


class Servie2{
    private int num = 0;

    public void set(String string){
        num = 0;
        if("a".equals(string)){
            num = 100;
            System.out.println("a set");

            try {
                //睡眠的目的是为了等待另一个线程修改num的值
                Thread.sleep(2000);
            } catch (InterruptedException e) {
                e.printStackTrace();
            }
        }else{
            num = 200;
            System.out.println("b set");
        }
        System.out.println("string="+string+";num="+num);

    }
}

class ThreadA extends Thread{
    private Servie2 servie2;

    public ThreadA(Servie2 servie2){
        this.servie2 = servie2;
    }

    @Override
    public void run() {
        servie2.set("a");
    }
}


class ThreadB extends  Thread{
    private Servie2 servie2;

    public ThreadB(Servie2 servie2){
        this.servie2 = servie2;
    }

    @Override
    public void run() {
        servie2.set("b");
    }
}
复制代码

结果:

图片

如果对set方法设置synchronized关键字进行修饰,比如:

package com.jiangxia.chap2;

/**
 * 线程的同步机制 成员变量不是线程安全的
 * author:jiangxia
 */

public class Demo2 {
    public static void main(String[] args) {
        Servie2 s = new Servie2();

        Thread t1 = new ThreadA(s);
        t1.start();

        Thread t2 = new ThreadB(s);
        t2.start();
    }
}


class Servie2{
    private int num = 0;

    //set 方法设置synchronized关键字
    synchronized public void set(String string){
        num = 0;
        if("a".equals(string)){
            num = 100;
            System.out.println("a set");

            try {
                //睡眠的目的是为了等待另一个线程修改num的值
                Thread.sleep(2000);
            } catch (InterruptedException e) {
                e.printStackTrace();
            }
        }else{
            num = 200;
            System.out.println("b set");
        }
        System.out.println("string="+string+";num="+num);

    }
}

class ThreadA extends Thread{
    private Servie2 servie2;

    public ThreadA(Servie2 servie2){
        this.servie2 = servie2;
    }

    @Override
    public void run() {
        servie2.set("a");
    }
}


class ThreadB extends  Thread{
    private Servie2 servie2;

    public ThreadB(Servie2 servie2){
        this.servie2 = servie2;
    }

    @Override
    public void run() {
        servie2.set("b");
    }
}

复制代码

结果:

图片

如果有两个线程同时操作业务对象中的成员变量,可能会产生非线程安全的问题,所以需要在方法前使用synchronized关键字进行修饰。

3、多个对象使用多个对象锁

Synchronized取到的锁都是对象锁,而不是把一段代码或者方法作为锁,所以哪个线程先执行Synchronized关键字修饰的方法,那么哪个线程就持有该方法所属对象的锁,其他的线程只能等待,前提是多个线程访问的是同一个对象。如果多个线程访问的是多个对象,那么jvm就会创建出多个对象锁。比如:

package com.jiangxia.chap2;
public class Demo3 {
    public static void main(String[] args) {
        Servie3 s1 = new Servie3();
        Servie3 s2 = new Servie3();

        Thread t1 = new Demo3ThreadA(s1);
        Thread t2 = new Demo3ThreadB(s2);
        t1.start();
        t2.start();
    }
}


class Servie3{
    private int num = 0;

    //set 方法设置synchronized关键字
    synchronized public void set(String string){
        num = 0;
        if("a".equals(string)){
            num = 100;
            System.out.println("a set");

            try {
                //睡眠的目的是为了等待另一个线程修改num的值
                Thread.sleep(2000);
            } catch (InterruptedException e) {
                e.printStackTrace();
            }
        }else{
            num = 200;
            System.out.println("b set");
        }
        System.out.println("string="+string+";num="+num);

    }
}

class Demo3ThreadA extends Thread{
    private Servie3 servie3;

    public Demo3ThreadA(Servie3 servie3){
        this.servie3 = servie3;
    }

    @Override
    public void run() {
        servie3.set("a");
    }
}


class Demo3ThreadB extends  Thread{
    private Servie3 servie3;

    public Demo3ThreadB(Servie3 servie3){
        this.servie3 = servie3;
    }

    @Override
    public void run() {
        servie3.set("b");
    }
}

复制代码

结果:

图片

4、Synchronized方法锁的是整个对象

比如:

package com.jiangxia.chap2;

public class Demo4 {
    public static void main(String[] args) {
        Demo4Service service = new Demo4Service();
        Thread t1 = new Demo4ThreadA(service);
        Thread t2 = new Demo4ThreadB(service);

        t1.start();
        t2.start();
    }
}
class Demo4Service{
    synchronized public void foo1(){
        System.out.println("开始运行foo1方法,threadname:"+Thread.currentThread().getName());
        try {
            Thread.sleep(2000);
        } catch (InterruptedException e) {
            e.printStackTrace();
        }
        System.out.println("foo1方法运行结束");
    }

    synchronized public void foo2(){
        System.out.println("开始运行foo2方法,threadname:"+Thread.currentThread().getName());
        try {
            Thread.sleep(2000);
        } catch (InterruptedException e) {
            e.printStackTrace();
        }
        System.out.println("foo2方法运行结束");
    }
}

class Demo4ThreadA extends  Thread{
    private Demo4Service service;
    public  Demo4ThreadA(Demo4Service service){
        this.service = service;
    }

    @Override
    public void run() {
        service.foo1();
    }
}


class Demo4ThreadB extends  Thread{
    private Demo4Service service;
    public  Demo4ThreadB(Demo4Service service){
        this.service = service;
    }

    @Override
    public void run() {
        service.foo2();
    }
}
复制代码

结果:

图片

A线程先持有object对象的对象锁,B线程就不可以以异步的方式调用object对象使用synchronized关键字修饰的方法,线程B只能等待线程A的方法执行完成释放对象锁才能够执行,也就是同步执行。

B线程可以以异步的方式调用object对象没有使用synchronized修饰的方法。比如:

package com.jiangxia.chap2;

public class Demo4 {
    public static void main(String[] args) {
        Demo4Service service = new Demo4Service();
        Thread t1 = new Demo4ThreadA(service);
        Thread t2 = new Demo4ThreadB(service);

        t1.start();
        t2.start();
    }
}
class Demo4Service{
    synchronized public void foo1(){
        System.out.println("开始运行foo1方法,threadname:"+Thread.currentThread().getName());
        try {
            Thread.sleep(2000);
        } catch (InterruptedException e) {
            e.printStackTrace();
        }
        System.out.println("foo1方法运行结束");
    }

     public void foo2(){
        System.out.println("开始运行foo2方法,threadname:"+Thread.currentThread().getName());
        try {
            Thread.sleep(2000);
        } catch (InterruptedException e) {
            e.printStackTrace();
        }
        System.out.println("foo2方法运行结束");
    }
}

class Demo4ThreadA extends  Thread{
    private Demo4Service service;
    public  Demo4ThreadA(Demo4Service service){
        this.service = service;
    }

    @Override
    public void run() {
        service.foo1();
    }
}


class Demo4ThreadB extends  Thread{
    private Demo4Service service;
    public  Demo4ThreadB(Demo4Service service){
        this.service = service;
    }

    @Override
    public void run() {
        service.foo2();
    }
}
复制代码

结果:

图片

5、脏读

脏读表示在多个线程共享同一个对象,进行了修改对象中属性,当读取对象属性的时候,发现属性被别的线程修改了.

假设一个很简单的例子,当一个被共享的对象有A,B,同时有setValue和geValue方法分别去修改和读取这两个值,当setValue方法加上同步锁synchronized,也就是说在修改的过程中是同步的,但是getValue方法不同步,也就是说当第一个进去的线程执行完了 再去执行getValue方法的时候,这个时候对象的两个属性值A,B可能被第二个进去的线程修改了,拿到的数据并不是想要的,这就是脏读。

比如:

package com.jiangxia.chap2;

public class Demo5 {
    public static void main(String[] args) throws InterruptedException {
        Demo05User user = new Demo05User();
        Thread t = new Demo05Thread(user);
        t.start();
        Thread.sleep(200);
        user.getValue();
    }
}

class Demo05User{
    private String username = "a";
    private String password = "aa";

    synchronized public void setUsernameAndPassword(String username, String password){
        this.username = username;
        try {
            Thread.sleep(2000);
        } catch (InterruptedException e) {
            e.printStackTrace();
        }
        this.password = password;
        System.out.println("setUsernameAndPassword方法,线程名称:" + Thread.currentThread().getName() +
                ",username=" +username + ", password=" + password);
    }

    synchronized public void getValue(){
        System.out.println("getValue方法,线程名称" + Thread.currentThread().getName() +
                ", username=" + username + ", password=" + password );
    }
}

class Demo05Thread extends Thread{
    private Demo05User user;
    public Demo05Thread(Demo05User user){
        this.user = user;
    }

    @Override
    public void run() {
        user.setUsernameAndPassword("B", "BB");
    }
}
复制代码

结果:

图片

package com.jiangxia.chap2;

public class Demo5 {
    public static void main(String[] args) throws InterruptedException {
        Demo05User user = new Demo05User();
        Thread t = new Demo05Thread(user);
        t.start();
        Thread.sleep(200);
        user.getValue();
    }
}

class Demo05User{
    private String username = "a";
    private String password = "aa";

    synchronized public void setUsernameAndPassword(String username, String password){
        this.username = username;
        try {
            Thread.sleep(2000);
        } catch (InterruptedException e) {
            e.printStackTrace();
        }
        this.password = password;
        System.out.println("setUsernameAndPassword方法,线程名称:" + Thread.currentThread().getName() +
                ",username=" +username + ", password=" + password);
    }

     public void getValue(){
        System.out.println("getValue方法,线程名称" + Thread.currentThread().getName() +
                ", username=" + username + ", password=" + password );
    }
}

class Demo05Thread extends Thread{
    private Demo05User user;
    public Demo05Thread(Demo05User user){
        this.user = user;
    }

    @Override
    public void run() {
        user.setUsernameAndPassword("B", "BB");
    }
}
复制代码

结果:

图片

当A线程调用使用synchronize关键字修饰的方法时,A线程就获取到一个方法锁,准确来说是获取到对象锁,所以其它线程必须等A线程执行完毕后才可以调用其它使用synchronized关键字修饰的方法。这时A线程已经执行完一个完整任务,也就是说username和password两个成员变量已经被同时赋值,所以这种情况下是不存在脏读的。

6、锁重入

可重入锁:自己可以再次获取自己的内部的锁。比如有线程A获得了某对象的锁,此时这个时候锁还没有释放,当其再次想获取这个对象的锁的时候还是可以获取的,如果不可锁重入的话,就会造成死锁。可重入锁也支持在父子类继承的环境中。

关键字Synchronized拥有锁重入的功能,就是说在使用synchronized时,当一个线程得到一个对象锁后,再次请求对象锁时是可以再次得到该对象锁。比如:

package com.jiangxia.chap2;
public class Demo6 {
    public static void main(String[] args) {
        Thread t = new Demo06Thread();
        t.start();
    }
}

class Demo06Service{
    synchronized public void foo1(){
        System.out.println("foo1方法");
        foo2();
    }
    synchronized public void foo2(){
        System.out.println("foo2方法");
        foo3();
    }
    synchronized public void foo3(){
        System.out.println("foo3方法");
    }
}

class Demo06Thread extends Thread{
    @Override
    public void run() {
        Demo06Service service = new Demo06Service();
        service.foo1();
    }
}
复制代码

结果:

图片

可重入锁也支持在父子类继承的环境中。比如:

public class Demo6 {
    public static void main(String[] args) {
        Thread t = new Demo06Thread();
        t.start();
    }
}

class Demo06Service{
    synchronized public void foo1(){
        System.out.println("foo1方法");
        foo2();
    }
    synchronized public void foo2(){
        System.out.println("foo2方法");
        foo3();
    }
    synchronized public void foo3(){
        System.out.println("foo3方法");
    }
}

class Demo06Thread extends Thread{
    @Override
    public void run() {
//        Demo06Service service = new Demo06Service();
//        service.foo1();
        //改用继承子类
        Demo06ServiceB serviceB = new Demo06ServiceB();
        serviceB.foo4();
    }
}

//继承
class Demo06ServiceB extends Demo06Service{
    synchronized public void foo4(){
        System.out.println("foo4方法");
        super.foo1();
    }
}
复制代码

结果:

图片

7、锁的自动释放

当一个线程执行的代码出现了异常,其所持有的锁应该会自动释放,比如:

public class Demo7 {
    public static void main(String[] args) {
        DemoService7 service7 = new DemoService7();
        Thread t1 = new DemoThread7(service7);
        t1.setName("A");
        t1.start();

        Thread t2 = new DemoThread7(service7);
        t2.start();
    }
}
class DemoService7{
    synchronized public void foo(){
        if("A".equals(Thread.currentThread().getName())){
            System.out.println("线程A开始于"+System.currentTimeMillis());
            while(true){
                if((""+ Math.random()).substring(0,8).equals("0.123456")){
                    System.out.println("线程A结束于:"+System.currentTimeMillis());
                    Integer.parseInt("A");
                }
            }
        }else{
            System.out.println("线程B开始于"+System.currentTimeMillis());
        }
    }
}
class DemoThread7 extends Thread{
    private DemoService7 service7;

    public DemoThread7(DemoService7 service7){
        this.service7 = service7;
    }

    @Override
    public void run() {
        service7.foo();
    }
}
复制代码

结果:

图片

8、同步不具有继承性

同步synchronized不具有继承性,比如:

public class Demo8 {
    public static void main(String[] args) {
        DemoService8B serviceB = new DemoService8B();
        Thread t1 = new Demo8Thread(serviceB);
        t1.setName("A");
        t1.start();

        Thread t2 = new Demo8Thread(serviceB);
        t2.setName("B");
        t2.start();
    }
}
class DemoService8A{
    synchronized public void foo(){
        try{
            System.out.println("父类:" + Thread.currentThread().getName() + ",开始于" + System.currentTimeMillis());
            Thread.sleep(5000);
            System.out.println("父类:" + Thread.currentThread().getName() + ",结束于" + System.currentTimeMillis());
        }catch(InterruptedException e){
            e.printStackTrace();
        }
    }
}

class DemoService8B extends  DemoService8A{
    public void foo(){
        try {
            System.out.println("子类:" + Thread.currentThread().getName() + ",开始于" + System.currentTimeMillis());
            Thread.sleep(5000);
            System.out.println("子类:" + Thread.currentThread().getName() + ",结束于" + System.currentTimeMillis());
            super.foo();
        }catch (InterruptedException e){
            e.printStackTrace();
        }
    }
}

class Demo8Thread extends Thread{
    DemoService8B service;
    public Demo8Thread(DemoService8B service){
        this.service = service;
    }

    @Override
    public void run() {
        service.foo();
    }
}
复制代码

结果:

图片

3 总结

以上就是Java多线程编程中synchronized关键字修饰方法的一些内容,synchronized关键字不仅可以修饰方法,还可以修饰代码块。后面一篇文章将会介绍synchronized关键字修饰方法的缺点以及它修饰代码块的相关内容。

如果觉得这篇文章不错对你有帮助,就点赞分享给更多的人吧!

相关推荐: