java看不见的坑

java看不见的坑

1. 锁的虚假唤醒

现在有这么一个需求 : 线程交替打印0,1,0,1 ...

public class LockTest {

    private int num = 0;

    public synchronized  void add() {

        for(int a=1;a<=50;a++){
            if(num != 0){
                try {
                    this.wait();
                } catch (InterruptedException e) {
                    e.printStackTrace();
                }
            }
            num++;
            System.out.println(Thread.currentThread().getName()+" - num : "+num);
            this.notifyAll();
        }
    }

    public synchronized  void cut()  {

        for(int a=1;a<=50;a++) {
            if(num == 0){
                try {
                    this.wait();
                } catch (InterruptedException e) {
                    e.printStackTrace();
                }
            }
            num--;
            System.out.println(Thread.currentThread().getName() + " - num : " + num);
            this.notifyAll();
        }

    }

    public static void main(String[] args) throws InterruptedException{
        LockTest test = new LockTest();
        new Thread(() -> { test.add();},"A").start();
        new Thread(() -> { test.cut();},"B").start();
        // new Thread(() -> { test.cut();},"C").start();
    }
}

如果就开两个线程,那么段代码很符合需求 ,如果在加入第三个线程,数据就发生了错乱.

根据java api Object #wait()描述

在一个参数版本中,中断和虚假唤醒是可能的,并且该方法应该始终在循环中使用: 

  synchronized (obj) {
         while (<condition does not hold>)
             obj.wait();
         ... // Perform action appropriate to condition
     } 该方法只能由作为该对象的监视器的所有者的线程调用。 有关线程可以成为监视器所有者的方式的说明,请参阅notify方法。 
i.e. 简单理解,就是如果使用if做判断 : 唤醒时是不会再次判断数值是否为0或1,而是直接执行.如果改为while,则会再次判断数值,所以不会发生累加或者累减得可能

不会发生虚假唤醒的版本 改为 :

public class LockTest {

    private int num = 0;

    public synchronized  void add() {

        while(num != 0){
            try {
                this.wait();
            } catch (InterruptedException e) {
                e.printStackTrace();
            }
        }
        num++;
        System.out.println(Thread.currentThread().getName() + " - num : " + num);
        this.notifyAll();
    }

    public synchronized  void cut()  {

        while(num == 0){
            try {
                this.wait();
            } catch (InterruptedException e) {
                e.printStackTrace();
            }
        }
        num--;
        System.out.println(Thread.currentThread().getName() + " - num : " + num);
        this.notifyAll();
    }

    public static void main(String[] args) throws InterruptedException{
        LockTest test = new LockTest();
        new Thread(() -> {
            for(int a=1;a<=50;a++){
                test.add();
            }
        },"A").start();
        new Thread(() -> {
            for(int a=1;a<=50;a++){
                test.cut();
            }
        },"B").start();
        new Thread(() -> {
            for(int a=1;a<=50;a++){
                test.add();
            }
        },"C").start();
    }
}

Lock锁版本 :

public class LockTest {

    private int num = 0;
    private Lock lock = new ReentrantLock();
    private Condition condition = lock.newCondition();

    public void add() {

        lock.lock();
        try {
            while(num != 0){
                condition.await();
            }
            num++;
            System.out.println(Thread.currentThread().getName() + " - num : " + num);
            condition.signalAll();
        } catch (InterruptedException e) {
            e.printStackTrace();
        } finally {
            lock.unlock();
        }
    }

    public void cut()  {

        lock.lock();
        try {
            while(num == 0){
                condition.await();
            }
            num--;
            System.out.println(Thread.currentThread().getName() + " - num : " + num);
            condition.signalAll();
        } catch (InterruptedException e) {
            e.printStackTrace();
        } finally {
            lock.unlock();
        }
    }

    public static void main(String[] args){
        LockTest test = new LockTest();
        new Thread(() -> {
            for(int a=1;a<=50;a++){
                test.add();
            }
        },"A").start();
        new Thread(() -> {
            for(int a=1;a<=50;a++){
                test.cut();
            }
        },"B").start();
        new Thread(() -> {
            for(int a=1;a<=50;a++){
                test.add();
            }
        },"C").start();
    }
}

! 将代码中去除if判断,改为while做判断.这样每次唤醒时会再次判断一次条件是否满足

2. 线程顺序执行

1. 问题一 : 线程间顺序调用

线程A/B/C ,A打印5次,B打印10次,C打印15次,依次打印, A->B->C->A->B-> ... 依次打印十次

class ShareResource{
    private int num = 1;
    private Lock lock = new ReentrantLock();
    private Condition c1 = lock.newCondition();
    private Condition c2 = lock.newCondition();
    private Condition c3 = lock.newCondition();


    public void print5(int outerLoop){
        lock.lock();
        try{
            while(num != 1){
                c1.await();
            }
            for(int a=1;a<=5;a++){
                System.out.println(Thread.currentThread().getName()+"\t innerLoop : "+a+"\t outerLoop : "+outerLoop);
            }
            num  = 2;
            c2.signal();
        } catch (InterruptedException e) {
            e.printStackTrace();
        } finally {
            lock.unlock();
        }

    }
    public void print10( int outerLoop){
        lock.lock();
        try{
            while(num != 2){
                c2.await();
            }
            for(int a=1;a<=10;a++){
                System.out.println(Thread.currentThread().getName()+"\t innerLoop : "+a+"\t outerLoop : "+outerLoop);
            }
            num  = 3;
            c3.signal();
        } catch (InterruptedException e) {
            e.printStackTrace();
        } finally {
            lock.unlock();
        }

    }
    public void print15(int outerLoop){
        lock.lock();

        try{
            while(num != 3){
                c3.await();
            }
            for(int a=1;a<=15;a++){
                System.out.println(Thread.currentThread().getName()+"\t innerLoop : "+a+"\t outerLoop : "+outerLoop);
            }
            num  = 1;
            c1.signal();
        } catch (InterruptedException e) {
            e.printStackTrace();
        } finally {
            lock.unlock();
        }

    }

}

public class TestLockLoop {
    public static void main(String[] args) {
        ShareResource shareResource = new ShareResource();
        new Thread(() ->{
            for(int a=1; a<= 10;a++){
                shareResource.print5(a);
            }
        },"A").start();
        new Thread(() ->{
            for(int a=1; a<= 10;a++){
                shareResource.print10(a);
            }
        },"B").start();
        new Thread(() ->{
            for(int a=1; a<= 10;a++){
                shareResource.print15(a);
            }
        },"C").start();
    }

}

2. 问题二 : 线程锁对象

下面代码先执行谁呢?

class Phone
{
	public static synchronized void sendEmail() throws Exception
	{
		TimeUnit.SECONDS.sleep(4);
		System.out.println("------sendEmail");
	}
	
	public synchronized void getSMS() throws Exception
	{
		System.out.println("-----getSMS");
	}
	
	public void getHello()
	{
		System.out.println("-----getHello");
	}
}
public static void main(String[] args) throws Exception
	{
		Phone phone = new Phone();
		Phone phone2 = new Phone();    //p1/p2 ==--> Phone.class
		
		new Thread(() -> {
			try 
			{
				phone.sendEmail();
			} catch (Exception e) {
				e.printStackTrace();
			}
		}, "A").start();
		
		Thread.sleep(100);
		
		new Thread(() -> {
			try 
			{
				//phone.getSMS();
				//phone.getHello();
				phone1.getSMS();
			} catch (Exception e) {
				e.printStackTrace();
			}
		}, "B").start();

	}

答案是 : 先等待sendEmail()执行完,才会执行getSMS().

明明是两个不同的线程操作不同的方法,

为什么会等待第一个线程调用完方法才会调用下一个方法呢?

一个对象里面如果有多个synchronized方法,某一个时刻内,只要一个线程去调用其中的一个synchronized方法了,
其它的线程都只能等待,
换句话说,
某一个时刻内,只能有唯一一个线程去访问这些synchronized方法
锁的是当前对象this,被锁定后,其它的线程都不能进入到当前对象的其它的synchronized方法

加个普通方法后发现和同步锁无关
换成两个对象后,不是同一把锁了,情况立刻变化。


都换成静态同步方法后,情况又变化
所有的非静态同步方法用的都是同一把锁——实例对象本身,

synchronized实现同步的基础:Java中的每一个对象都可以作为锁。
具体表现为以下3种形式。
对于普通同步方法,锁是当前实例对象。
对于静态同步方法,锁是当前类的Class对象。
对于同步方法块,锁是Synchonized括号里配置的对象。

当一个线程试图访问同步代码块时,它首先必须得到锁,退出或抛出异常时必须释放锁。

也就是说如果一个实例对象的非静态同步方法获取锁后,该实例对象的其他非静态同步方法必须等待获取锁的方法释放锁后才能获取锁,
可是别的实例对象的非静态同步方法因为跟该实例对象的非静态同步方法用的是不同的锁,
所以毋须等待该实例对象已获取锁的非静态同步方法释放锁就可以获取他们自己的锁。

所有的静态同步方法用的也是同一把锁——类对象本身,
这两把锁是两个不同的对象,所以静态同步方法与非静态同步方法之间是不会有竞态条件的。
但是一旦一个静态同步方法获取锁后,其他的静态同步方法都必须等待该方法释放锁后才能获取锁,
而不管是同一个实例对象的静态同步方法之间,
还是不同的实例对象的静态同步方法之间,只要它们同一个类的实例对象!

3. 基本类型与引用类型区别

public class TestType {

    private static void getInt(int a){
        a = 3;
    }
    private static void getUser(User user){
        user.setId(3);
    }
    private static void getString(String s){
        s = "3";
    }
    public static void main(String[] args) {
        // 基本类型
        int a = 1;
        getInt(1);
        System.out.println(a); // 输出 : 1
        // 引用类型
        User user = new User();
        user.setId(1);
        getUser(user);
        System.out.println(user); // 输出 : User(id=3, name=null)
        // 字符串
        String s = "1";
        getString(s);
        System.out.println(s); // 输出 : 1

    }
}

通过这个示例,可以证明 :

  1. 基本类型,相当于在main()中复制一份作为方法参数传递

  2. 引用类型是不会发生复制,直接使用当前的实例进行传递

  3. 字符串类型比较特殊,它虽然是引用类型,但它也会发生复制,原因是它的引用存在常量池,把值改变了,但是引用是没有发生变化的.

4. 静态代码块,代码块,构造器执行顺序

  1. 如果包含public类和非public类,执行顺序如下 :
    1. 非public类静态代码块
    2. public类的静态代码块
    3. public类的代码块
    4. public类的构造方法
    5. 非public类的代码块
    6. 非public类的构造方法

总结来说 : 就是先执行静态代码块 ->代码块->构造器

class Code{
    public Code(){
        System.out.println("Code的构造方法1111");
    }
    {
        System.out.println("Code的代码块2222");
    }
    static {
        System.out.println("Code的静态代码块3333");
    }
}
public class TestOrder{   //TestOrder.class--->static block---
    {
        System.out.println("TestOrder的代码块444");
    }
    static {
		System.out.println("TestOrder的静态代码块555");
	}
    public TestOrder(){
        System.out.println("TestOrder的构造方法666");
    }
    public static void main(String[] args){
        System.out.println("======我是美丽分割线=========TestOrder的main方法 777");
        new Code();
        new TestOrder();
    }
}

输出如下 :

TestOrder的静态代码块555
======我是美丽分割线=========TestOrder的main方法 777
Code的静态代码块3333
Code的代码块2222
Code的构造方法1111
TestOrder的代码块444
TestOrder的构造方法666
  1. 如果有父类关系 :
    1. 第一执行顺序 : 父类先执行 ,其次是子类
    2. 第二执行顺序 : 静态代码块 -> 代码块 -> 构造器
    3. static修饰的方法或者只会加载一次,所以只会执行一次
class Father   ///Father.class--Son.class -Father Instance--Son Instance
{
    public Father(){
        System.out.println("Father 构造器111111");
    }
    {
        System.out.println("Father 代码块222222");
    }
    static{
        System.out.println("Father 静态代码块333333");
    }
}
class Son extends Father
{
    public Son()
    {
        System.out.println("Son 构造器444444");
    }
    {
        System.out.println("Son 代码块555555");
    }
    static{
        System.out.println("Son 静态代码块666666");
    }
}
public class TestStaticSeq
{
    public static void main(String[] args)
    {
        new Son();//从父到子,静态先行
        System.out.println("============ 美丽的分割线 ==========");
        new Son();
        System.out.println("============ 美丽的分割线 ==========");
        new Father();
    }
}

代码执行如下:

Father 静态代码块333333
Son 静态代码块666666
Father 代码块222222
Father 构造器111111
Son 代码块555555
Son 构造器444444
============ 美丽的分割线 ==========
Father 代码块222222
Father 构造器111111
Son 代码块555555
Son 构造器444444
============ 美丽的分割线 ==========
Father 代码块222222
Father 构造器111111

java看不见的坑
https://www.blaaair.com/archives/java-kan-bu-jian-de-keng
作者
Glo6f
发布于
2024年01月27日
许可协议