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=3Thread-2 计算, count=2Thread-1 计算, count=3Thread-4 计算, count=0Thread-3 计算, count=1

可以看到,Thread-0Thread-1 同时打印的 count 都是3,说明两个线程是一起执行 run 方法的,即同时减1,会产生非线程安全的问题,而我们理想的结果是,每个结果都是依次递减的,如果要解决这个问题,可以使用 synchronized 关键字来修饰方法

二、什么是synchronized同步方法

如果一个方法用 synchronized 修饰符来修饰,那么该方法称为同步方法,即如果有多个线程在同一时刻调用该方法,那么同一时刻只能有一个线程执行该方法,其他线程只能等待,等待前一个线程执行完同步方法之后再去执行该方法

与之对应的是异步方法,如果多个线程在同一个时刻调用该方法,那么同一时刻会有很多线程来调用该方法,不会存在后面的线程等待前面的线程执行完再执行的情况

锁机制

在 java 中,每个对象都拥有一个锁标记,也称为监视器,多线程同时访问某个对象时,线程只有获得了该对象的锁才能访问其中的方法

synchronized 可以在任意对象和方法上加锁,加锁的这段代码称为 互斥区 或者 临界区,当一个线程调用指定对象的同步方法时,线程首先要尝试去拿到调用对象的对象锁,如果能够拿到这把对象锁,那么这个线程就可以执行 synchronized 里面的代码;其他线程,因为没有拿到对象锁,因此不能访问调用对象里的同步方法,只能等待,只有等前一个线程执行完,它才会释放持有的对象锁,其他线程拿到锁后,才能调用对象的同步方法

我们再来看一个线程安全的例子,在 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=4Thread-1 计算, count=3Thread-3 计算, count=2Thread-2 计算, count=1Thread-4 计算, count=0

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

当我们在方法前面加上 synchronized 关键字的时候,使得多个线程在执行 run 方法的时候,以排队的方式进行处理。这个例子中,5个线程同时调用同一个对象 thread,当第一个线程 Thread-0 执行 thread 对象的同步方法时,即持有了对象锁 thread,其他方法由于没有对象锁,只能等待 Thread-0 执行完同步方法并释放对象锁之后再执行,之后的4个线程都是按照这种模式来运行的

三、synchronized同步方法和线程安全

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

非线程安全存在于实例变量中的,如果是方法内部的变量,则是线程安全的,看个例子

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 和线程 ThreadA

//创建线程类 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();
        }
    }

    public static void main(String[] args) {
        HashSelPrivateNum hashSelPrivateNum = new HashSelPrivateNum();
        ThreadA threadA = new ThreadA(hashSelPrivateNum);
        threadA.start();

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

结果是:

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

可以看到,此时虽然类 HashSelPrivateNum 的方法 addI 不是同步方法,且不是按照代码的顺序输出的,但是当线程 A 和线程 B 调用的时候,因为此时的变量 num 是方法内部的变量,每个线程都有自己的一个变量 num,因此,每个线程只能对自己对应的那个变量 num 赋值,所以不会造成数值的覆盖或者重复等,是线程安全的,这是变量 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 变量也出现了重复的情况,究其原因,就是线程 thread a 执行完 if 语句还没有输出的时候,线程 thread b 也进入这个方法,执行 if 语句并且重新为实例变量 num 赋值为 200,然后 thread b 输出变量的值就是200,然后线程 thread a 再执行,因为共有变量 num 已经修改过了,因此线程 thread a 输出变量的值还是 200,此时输出的是 脏数据,即修改之后的数据,因此出现了 非线程安全 的问题

对于这个问题,我们只需要在 addI 方法前面添加 synchronized 关键字即可

class HashSelPrivateNum {

    private int num = 0;

    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

可以看到,线程 thread a 先执行同步方法 addI() ,同时拥有了对象锁 hashSelPrivateNum,那么线程 thread-b 只能等待 thread a 执行完同步方法,释放对象锁之后,才可以获取对象锁,然后执行该对象的同步方法

此时addI 是同步方法,哪个线程先拿到对象锁,就先执行同步方法,没有拿到对象锁的线程,只能等待前者执行完同步方法之后才可以执行,此时就不会发生数据的脏读

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

三、多个对象多个锁

创建 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();
        }
    }

    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

可以看到,多个线程访问多个对象,该实例创建了 2 个 HashSelPrivateNum 类的对象,就产生了 2 个对象锁,当线程 Thread A1 执行 synchronized 方法 addI(),便持有该方法所属对象 numRef1 的锁,此时线程 B 并不用等待,因为持有对象 numRef2 的锁,与 numRef1 锁无关。此时方法是异步执行的

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

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

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

四、脏读

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

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,这个时候与对象锁有关,不会出现脏读

五、锁重入

关键字 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多线程编程核心技术》

1. synchronized关键字在使用层面的理解 synchronized关键字是Java中用来实现线程同步的关键字,可以修饰方法和代码块。当线程访问被synchronized修饰的方法或代码块时,需要获取对象的锁,如果该锁已被其他线程获取,则该线程会进入阻塞状态,直到获取到锁为止。synchronized关键字可以保证同一时刻只有一个线程能够访问被锁定的方法或代码块,从而避免了多线程并发访问时的数据竞争和一致性问题。 2. synchronized关键字在字节码中的体现 在Java代码编译成字节码后,synchronized关键字会被编译成monitorenter和monitorexit指令来实现。monitorenter指令对应获取锁操作,monitorexit指令对应释放锁操作。 3. synchronized关键字在JVM中的实现 在JVM中,每个对象都有一个监视器(monitor),用来实现对象锁。当一个线程获取对象锁后,就进入了对象的监视器中,其他线程只能等待该线程释放锁后再去竞争锁。 synchronized关键字的实现涉及到对象头中的标志位,包括锁标志位和重量级锁标志位等。当一个线程获取锁后,锁标志位被设置为1,其他线程再去获取锁时,会进入自旋等待或者阻塞等待状态,直到锁标志位被设置为0,即锁被释放后才能获取锁。 4. synchronized关键字在硬件方面的实现 在硬件层面,锁的实现需要通过CPU指令和总线锁来实现。当一个线程获取锁时,CPU会向总线发送一个锁请求信号,其他CPU收到该信号后会进入自旋等待状态,直到锁被释放后才能获取锁。总线锁可以保证多个CPU之间的原子操作,从而保证锁的正确性和一致性。
评论 1
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

当前余额3.43前往充值 >
需支付:10.00
成就一亿技术人!
领取后你会自动成为博主和红包主的粉丝 规则
hope_wisdom
发出的红包
实付
使用余额支付
点击重新获取
扫码支付
钱包余额 0

抵扣说明:

1.余额是钱包充值的虚拟货币,按照1:1的比例进行支付金额的抵扣。
2.余额无法直接购买下载,可以购买VIP、付费专栏及课程。

余额充值