Java基础(线程方法,线程同步,死锁)

1 sleep方法

线程类Thread中的sleep方法:

public static native void sleep(long millis) throws InterruptedException;

该静态方法可以让当前执行的线程暂时休眠指定的毫秒数:

public class Test {

    public static void main(String[] args) {


        Thread t1 = new Thread("t1线程"){
            @Override
            public void run() {
                try {
                    //t1线程休眠10毫秒
                    Thread.sleep(10);
                } catch (InterruptedException e) {
                    e.printStackTrace();
                }
            }
        };
		
        //t1还没有启动,这里肯定是NEW状态
        System.out.println(t1.getState());
		
        //启动t1线程
        t1.start();
		
        //在循环期间查看t1的状态1000次
        //这里t1的状态可能是RUNNABLE,也可能是TIMED_WAITING,也可能是TERMINATED
        for (int i = 0; i < 1000; i++) {
            System.out.println(t1.getState());
        }
		
      
    }

}

//运行结果: 可以多运行几次,每次结果可能不一样
NEW
RUNNABLE
RUNNABLE
RUNNABLE
RUNNABLE
RUNNABLE
TIMED_WAITING
TIMED_WAITING
TIMED_WAITING
TIMED_WAITING
TIMED_WAITING
TIMED_WAITING
TIMED_WAITING
TIMED_WAITING
TIMED_WAITING
TIMED_WAITING
TIMED_WAITING
.....
RUNNABLE
RUNNABLE
TERMINATED
TERMINATED
TERMINATED
TERMINATED
.....

可以看出,线程执行了sleep方法后,会从RUNNABLE状态进入到TIMED_WAITING状态

这时候线程所处的是一种阻塞状态,是之前介绍过的三种阻塞情况的其中一种

这种阻塞的特点是:阻塞结束后,线程会自动回到RUNNABLE状态

此时的状态图为:

在这里插入图片描述

2 join方法

线程类Thread中的join方法:

public final synchronized void join(long millis)throws InterruptedException{
    //...
}
    
public final void join() throws InterruptedException{
    //...
}

使用join方法,可以让当前线程阻塞,等待另一个指定的线程运行结束后,当前线程才可以继续运行:

例如,使用无参的join方法

public class Test {

    public static void main(String[] args) {


        Thread t1 = new Thread("t1线程"){
            @Override
            public void run() {
                try {
                    //t1线程睡眠1秒钟
                    Thread.sleep(1000);
                } catch (InterruptedException e) {
                    e.printStackTrace();
                }
                System.out.println("t1线程结束");
            }
        };

        Thread t2 = new Thread("t2线程"){
            @Override
            public void run() {
                try {
                    //t2线程调用t1.join方法
                    //t2线程进入阻塞状态
                    //t2线程要等到t1线程运行结束,才能恢复到RUNNABLE状态
                    t1.join();
                } catch (InterruptedException e) {
                    e.printStackTrace();
                }
                System.out.println("t2线程结束");
            }
        };

        t1.start();
        t2.start();

        //让主线程休眠500毫秒,目的是为了给t1和t2点时间,让他们俩个线程进入状态
        try {
            Thread.sleep(500);
        } catch (InterruptedException e) {
            e.printStackTrace();
        }

        System.out.println(t2.getState());

    }

}

//运行结果:
WAITING
t1线程结束
t2线程结束

t2线程中,调用了t1对象的join方法,那么t2线程就会阻塞,等待t1线程的运行结束,t2线程才能恢复

可以看出,线程执行了join()方法后,会从RUNNABLE状态进入到WAITING状态

但是如果线程中调用的是有参数的join方法,线程所处的状态就不一样了:

例如,使用有参的join方法,其他代码和上面一样

public class Test {

    public static void main(String[] args) {


        Thread t1 = new Thread("t1线程"){
            @Override
            public void run() {
                try {
                    //t1线程睡眠1秒钟
                    Thread.sleep(1000);
                } catch (InterruptedException e) {
                    e.printStackTrace();
                }
                System.out.println("t1线程结束");
            }
        };

        Thread t2 = new Thread("t2线程"){
            @Override
            public void run() {
                try {
                    //t2线程调用t1.join方法
                    //t2线程进入阻塞状态
                    //t2线程要等到t1线程运行结束,才能恢复到RUNNABLE状态
                    //2000表示,当前线程t2最多阻塞2秒钟,2秒钟之内t1线程没有结束,那么t2线程就自动恢复
                    t1.join(2000);
                } catch (InterruptedException e) {
                    e.printStackTrace();
                }
                System.out.println("t2线程结束");
            }
        };

        t1.start();
        t2.start();

        //让主线程休眠500毫秒,目的是为了给t1和t2点时间,让他们俩个线程进入状态
        try {
            Thread.sleep(500);
        } catch (InterruptedException e) {
            e.printStackTrace();
        }

        System.out.println(t2.getState());

    }

}

//运行结果:
TIMED_WAITING
t1线程结束
t2线程结束

可以看出,线程执行了join(long million)方法后,会从RUNNABLE状态进入到TIMED_WAITING状态

此时的状态图为:

线程A中,调用了线程B对象的join方法,那么线程A就会进入到阻塞状态,这种阻塞又俩种情况,一种是调用了无参的join方法,那么此时线程A的状态为WAITING(无限期等待),另一种是调用了有参的join方法,那么此时线程A的状态为TIMED_WAITING(有限期等待)

线程A如果调用了sleep方法,那么线程A也会进入阻塞状态,此时线程A的状态为TIMED_WAITING

总结:

  • 如果指定了时间,线程阻塞一定的时间后,会自动恢复到RUNNABLE状态,这种情况下,线程的状态为TIMED_WAITING(有限期等待)
  • 如果没有指定时间,线程会一直阻塞着,直到某个条件满足时,才会自动恢复,这种情况下,线程的状态为WAITING(无限期等待)

在这种情况下,其实还有另一种方式,可以让线程从阻塞状态恢复到RUNNABLE状态,那就是调用线程的interrupt方法

3 interrupt方法

线程类Thread中的interrupt方法:

//Interrupts this thread
public void interrupt(){
    //...
}

根据上面介绍sleep方法和join方法可知,这俩个方法都会抛出InterruptedException类型的异常,说明调用sleepjoin使线程进入阻塞状态的情况下,是有可能抛出InterruptedException类型的异常的。

InterruptedException异常类型指的是:线程A中,调用了线程B的interrupt方法,而此时线程B处于阻塞状态,那么此时sleep方法或者join方法就会抛出被打断的异常

public class Test {

    public static void main(String[] args) {


        Thread t1 = new Thread("t1线程"){
            @Override
            public void run() {
                try {
                    //t1线程休眠100秒
                    Thread.sleep(100000);
                } catch (InterruptedException e) {
                    e.printStackTrace();
                }
                System.out.println("t1线程结束");
            }
        };


        t1.start();

        //让主线程休眠500毫秒,目的是为了给t1时间,让它调用sleep方法而进入阻塞状态
        try {
            Thread.sleep(500);
        } catch (InterruptedException e) {
            e.printStackTrace();
        }

        //打断t1由于调用sleep方法而进入的阻塞状态
        t1.interrupt();


    }

}

//运行结果:
java.lang.InterruptedException: sleep interrupted
	at java.lang.Thread.sleep(Native Method)
	at com.briup.sync.Test$1.run(Test.java:11)
t1线程结束

可以看出,本来t1线程调用了sleep方法进入了阻塞状态,需要100后才会恢复的,但是我们在主线程中调用了t1线程对象的打断方法interrupt,那么此时 Thread.sleep(100000);这句代码就会抛出被打断的异常,同时t1线程从阻塞状态恢复到RUNNABLE状态,继续执行代码,输出了t1线程结束

此时对于的流程图为:

在这里插入图片描述

interrupt方法的工作原理:

interrupt方法是通过改变线程对象中的一个标识的值(true|false),来达到打断阻塞状态的效果。

一个线程在阻塞状态下,会时刻监测这个标识的值是不是true,如果一旦发现这个值变为true,那么就抛出异常结束阻塞状态,并再把这个值改为false

从Thread类的源码中可以看到:

interrupt方法中其实是调用了interrupt0这个本地方法,而interrupt0的注释为:Just to set the interrupt flag

public void interrupt() {
    if (this != Thread.currentThread())
        checkAccess();

    synchronized (blockerLock) {
        Interruptible b = blocker;
        if (b != null) {
            interrupt0();           // Just to set the interrupt flag
            b.interrupt(this);
            return;
        }
    }
    interrupt0();
}

private native void interrupt0();

可以看出,interrupt方法只是改变了线程对象中一个标识flag的值

查看线程对象中“打断标识”值的俩个方法:

线程类Thread中的isInterrupted方法:

public boolean isInterrupted() {
    return isInterrupted(false);
}
/**
 * Tests if some Thread has been interrupted.  The interrupted state
 * is reset or not based on the value of ClearInterrupted that is
 * passed.
 */
private native boolean isInterrupted(boolean ClearInterrupted);

注意,这个非静态方法,只是返回这个“打断标识”值,并且不会对这个值进行清除(true->false),因为所传参数ClearInterrupted的值为false

例如,默认情况下,一个线程对象中的“打断标识”值为false

public class Test {

    public static void main(String[] args) {


        Thread t1 = new Thread("t1线程"){
            @Override
            public void run() {
                for (int i = 0; i < 10; i++) {
                    //判断是否有其他线程调用了自己的interrupt方法
                    //调用类中的非静态方法:isInterrupted
                    System.out.println(this.isInterrupted());
                }
                System.out.println("t1线程结束");
            }
        };


        t1.start();

    }

}

//运行结果:
false
false
false
false
false
false
false
false
false
false
t1线程结束


无论线程是否处于阻塞状态,其他线程都可以调用这个线程的interrupt方法,因为该方法只是改变线程对象中“打断标识”值而已

例如,

public class Test {

    public static void main(String[] args) {


        Thread t1 = new Thread("t1线程"){
            @Override
            public void run() {
                for (int i = 0; i < 10; i++) {
                    //判断是否有其他线程调用了自己的interrupt方法
                    //调用类中的非静态方法:isInterrupted
                    System.out.println(this.isInterrupted());
                }
                System.out.println("t1线程结束");
            }
        };


        t1.start();

        t1.interrupt();


    }

}

//运行结果:
true
true
true
true
true
true
true
true
true
true
t1线程结束

可以看出,吊用了t1.interrupt();后,t1线程中的“打断标识”值设置为了true,可以通过线程对象中的isInterrupted方法返回这个标识的值,并且不会修改这个值,所以输出显示的一直是ture

线程类Thread中的interrupted方法:

public static boolean interrupted() {
    return currentThread().isInterrupted(true);
}
/**
 * Tests if some Thread has been interrupted.  The interrupted state
 * is reset or not based on the value of ClearInterrupted that is
 * passed.
 */
private native boolean isInterrupted(boolean ClearInterrupted);

注意,这个静态方法,返回这个“打断标识”值,并且对这个值进行清除(true->false),因为所传参数ClearInterrupted的值为true

例如,

public class Test {

    public static void main(String[] args) {


        Thread t1 = new Thread("t1线程"){
            @Override
            public void run() {
                for (int i = 0; i < 10; i++) {
                    //判断是否有其他线程调用了自己的interrupt方法
                    //调用类中的静态方法:interrupted
                    System.out.println(Thread.interrupted());
                }
                System.out.println("t1线程结束");
            }
        };


        t1.start();

        t1.interrupt();


    }

}

//运行结果:
true
false
false
false
false
false
false
false
false
false
t1线程结束

可以看出,第一次返回true之后,后面在调用方法查看这个“打断标识”值,都是false,因为静态方法 interrupted返回true后,会直接把这个值给清除掉。(true->false)

Thread类中的三个方法:interrupt()、isInterrupted()、interrupted()的结构关系大致如下:

public class Thread{
    
    public void interrupt() {
        //...
        interrupt0(); // Just to set the interrupt flag
    }
    private native void interrupt0();
    
    public boolean isInterrupted() {
        return isInterrupted(false);
    }
    
    public static boolean interrupted() {
        return currentThread().isInterrupted(true);
    }
    
   /**
     * Tests if some Thread has been interrupted.  The interrupted state
     * is reset or not based on the value of ClearInterrupted that is
     * passed.
     */
    private native boolean isInterrupted(boolean ClearInterrupted);
    
    public static native Thread currentThread();  
    
}

根据上面的代码结构,还有前面的一些示例以及解释,这个三个名字很像的方法,就应该容易分清楚了。

4 线程安全

JVM内存中的堆区,是一个共享的区域,是所有线程都可以访问的内存空间。

JVM内存中的栈去,是线程的私有空间,每个线程都有自己的栈区,别的先无法访问到自己栈区的数据。

我们之前编写的代码只有一个main线程,只有它自己去访问堆区中对象的数据,自然不会有什么问题。

但是在多线程环境中,如果有俩个线程并发访问堆区中一个对象中的数据,那么这个数据可能会出现和预期结果不符的情况,例如

public class Test {

    public static void main(String[] args) {

        final MyData myData = new MyData();

        Thread t1 = new Thread("t1"){
            @Override
            public void run() {
                String name = Thread.currentThread().getName();
                for (int i = 0; i < 10; i++) {
                    //先给num赋值
                    myData.num = i;
                    //然后再输出num的值
                    System.out.println(name + ": " + myData.num);
                }

            }
        };

       
        t1.start();
        
    }

}

class MyData{
    int num;
}

//运行结果:
t1: 0
t1: 1
t1: 2
t1: 3
t1: 4
t1: 5
t1: 6
t1: 7
t1: 8
t1: 9

这时候,t1线程自己访问堆区中的myData对象里面的num数据值,程序的结果每次都是和预期的一样

但是,如果再加一个线程t2,同时也访问这个堆区中myData对象中的num属性值,结果可能和预期的不一样:

public class Test {

    public static void main(String[] args) {

        MyData myData = new MyData();

        Thread t1 = new Thread("t1"){
            @Override
            public void run() {
                String name = Thread.currentThread().getName();
                for (int i = 0; i < 10; i++) {
                    //先给num赋值
                    myData.num = i;
                    //然后再输出num的值
                    System.out.println(name + ": " + myData.num);
                }

            }
        };

        Thread t2 = new Thread("t2"){
            @Override
            public void run() {

                for (int i = 100; i < 20000; i++) {
                    //给num赋值
                    myData.num = i;
                }

            }
        };

        t1.start();
        t2.start();



    }

}

class MyData{
    int num;
}
//运行结果:
t1: 0
t1: 11706
t1: 13766
t1: 15459
t1: 17710
t1: 19304
t1: 6
t1: 7
t1: 8
t1: 9

可以看到,每次运行的结果中,t1线程输出的num的值可能和预期都不一样

此时的内存图为:

在这里插入图片描述

注意1,t1和t2并发访问的时候,争夺CPU的时间片,运行完时间片,退出后再次争夺下一个时间片,也就是说t1和t2都是“断断续续”的运行的

注意2,在这期间,可能t1线程有一次拿到时间片运行的时候,给num赋值为1,然后时间片用完退出了,结果下次t2线程拿到了时间片,又将num的值赋成了11750,然后t1线程又拿到了时间片,本来预期的是输出1,但是结果却是输出了11750

核心的原因是,t1线程操作一下变量num,然后时间片用完退出去,t2先过来又操作了变量num,等t1线程再过来的时候,这值已经被t2线程给“偷偷”修改了,那么就出现了和预期不符的情况

总结:

如果有多个线程,它们在一段时间内,并发访问堆区中的同一个变量,并且有写入的操作,那么最终可能会出数据的结果和预期不符的情况,这种情况就是线程安全问题。

我们经常会进行这样的描述:这段代码是线程安全的,那段代码是非线程安全的。 其实就是在说,这段代码在多线程并发访问的环境中,是否会出现上述情况,也就是结果和预期不符的情况。

例如,观察下面代码,是线程安全的还是非线程安全的? t1线程每次输出的结果是否有出现和预期不符的情况?

public class Test {

    public static void main(String[] args) {


        Thread t1 = new Thread("t1"){
            @Override
            public void run() {
                String name = Thread.currentThread().getName();
                //每次循环改变变量i的值
                for (int i = 0; i < 10; i++) {
                    //输出变量i的值
                    System.out.println(name + ": " + i);
                }

            }
        };

        Thread t2 = new Thread("t2"){
            @Override
            public void run() {
				//每次循环改变变量i的值
                for (int i = 100; i < 20000; i++) {

                }

            }
        };

        t1.start();
        t2.start();



    }

}

每次执行结果都是一样的,和预期的相同,因为t1和t2俩个线程根本就没访问同一个共享变量!相当于t1和t2都是各自操作各自的变量i

思考,方法中的局部变量和对象中的成员变量分别在内存中什么地方?哪些变量可能被多个线程共享?

思考,如果t1和t2先不是“争先恐后”的并发访问变量num,而是排好队一个执行完,另一个在执行,是不是就可以解决这个线程安全问题了呢?

5 线程同步

当使用多个线程访问同一个共享变量的时候,并且线程中对变量有写的操作,这时就容易出现线程安全问题。

Java中提供了线程同步的机制,来解决上述的线程安全问题。

Java中实现线程同步的方式,是给需要同步的代码进行synchronized关键字加锁。

例如,改造之前有线程安全问题的代码,给需要同步的代码使用synchronized加锁

public class Test {

    public static void main(String[] args) {

        MyData myData = new MyData();

        Thread t1 = new Thread("t1"){
            @Override
            public void run() {

                String name = Thread.currentThread().getName();
                
                synchronized (myData){
                    for (int i = 0; i < 10; i++) {
                        myData.num = i;
                        System.out.println(name + ": " + myData.num);
                    }
                }

            }
        };

        Thread t2 = new Thread("t2"){
            @Override
            public void run() {

                synchronized (myData){
                    for (int i = 100; i < 20000; i++) {
                        myData.num = i;
                    }
                }

            }
        };

        t1.start();
        t2.start();

    }

}

class MyData{
    int num;
}

现在运行这个程序,每次输出的结果都是和预期的一样:t1线程每次输出的值都是0~9

分析:

线程同步的效果,就是一段加锁的代码,每次只能有一个拿到锁的线程,才有资格去执行,没有拿到的锁的线程,只能等拿到锁的线程把代码执行完,再把锁给释放了,它才能去拿这个锁然后再运行代码。

这样以来,本来这段代码是俩线程并发访问,“争先恐后”的去执行的,现在线程同步之后,这段代码就变成了先又一个拿到的锁的先去执行,执行完了,再由另一个线程拿到锁去执行。

相当于是大家每个线程不要抢,排好队一个一个去执行,那么这时候共享的变量的值,肯定不会出现线程安全问题!

例如,synchronized修饰代码块的使用格式为:

synchronized (锁对象){
    //操作共享变量的代码,这些代码需要线程同步,否则会有线程安全问题
    //...
}

对应这样加锁的代码,如果俩个线程进行并发访问的话:

  • 假设线程t1是第一个这段代码的线程,那么它会率先拿到这把锁,其实就是在这个锁对象中写入自己线程的信息,相当于告诉其他线程,这把锁现在是我的,你们都不能使用。
  • 这时候t1线程拿着锁,就可以进入到加锁的代码块中,去执行代码,执行很短的一个时间片,然后退出,但是锁并不释放,也就意味着,及时下次是t2线程抢到CPU的使用权,它也无法运行代码,因为t2没有拿到锁。
  • 就这样,t1线程开心的拿着锁,抢到CPU的执行权,抢到了就去执行,抢不到也不用担心,因为没有其他线程可以“偷偷”的执行这段代码,因为其他线程拿不到锁。
  • 而对于t2线程来说,即使有一次抢到了CPU执行权,来到了代码面前,要执行的时候才发现,锁被t1线程拿走了,自己无法进入代码块中执行,这时候t2线程就会从运行状态进入阻塞状态,直到t1运行完,把锁释放了,t2线程才会恢复到RUNNABLE状态,抢到CPU执行权,再拿到锁,然后进入代码块中执行

注意,这时候t2线程的阻塞状态,和之前学习的调用sleep或join方法进入的阻塞不同,这种阻塞属于锁阻塞,需要等待另一个线程把锁释放了,t2线程才能恢复。如果t2线程处于这种阻塞,那么调用线程对象的getState方法返回的状态名称为:BLOCKED

例如:

public class Test {

    public static void main(String[] args) {

       Object obj = new Object();

        Thread t1 = new Thread("t1"){
            @Override
            public void run() {

                synchronized (obj){
                    try {
                        Thread.sleep(100000);
                    } catch (InterruptedException e) {
                        e.printStackTrace();
                    }
                }

            }
        };

        Thread t2 = new Thread("t2"){
            @Override
            public void run() {

                synchronized (obj){

                }

            }
        };

        t1.start();

        //主线程休眠1秒钟,给t1线程点时间,让他先拿到锁,然后去休眠100秒
        try {
            Thread.sleep(1000);
        } catch (InterruptedException e) {
            e.printStackTrace();
        }

        t2.start();

        System.out.println("t1线程状态:"+t1.getState());
        System.out.println("t2线程状态:"+t2.getState());


    }

}
//运行结果:
t1线程状态:TIMED_WAITING
t2线程状态:BLOCKED

注意1,t1线程需要拿到锁对象obj,才能运行加锁的代码块

注意2,t2线程也需要拿到锁对象obj,才能运行加锁的代码块

注意3,锁对象obj只有一个,所以t1和t2只能有一个线程先拿到,拿到后执行代码,那么另一个就拿不到了,拿不到就阻塞,此时线程的状态为:BLOCKED

注意4,java中,任意一个对象,只要是对象,就可以用来当做,加锁代码块中的锁对象。然后让多个线程去抢这拿这把锁就可以了,此时就达到了线程同步的效果,因为拿到的锁线程能执行代码,其他拿不到的线程就不执行,并且进入阻塞状态。

此时的线程状态图为:

在这里插入图片描述

可以看出,假设t1线程拿到了锁,t2线程没拿到锁,那么t2线程就会因为锁不可用,进入到锁阻塞状态,直到t1先把加锁的代码执行完,把锁释放了,锁变的可用了,这是t2线程会自动恢复到RUNNABLE状态

注意,t1线程“拿到”锁,只是一种形象的说,就是我们之前说的 引用“指向”对象一样。其实是t1线程把自己的信息写入到了锁对象中,用这种方式告诉其他线程,这个锁对象已经被我 "拿走"了

6 synchronized

通过上面线程同步例子的讲解,我们已经知道了synchronized修饰一个代码块,并指定谁是锁对象的用法,除此之外,还可以使用synchronized直接修饰一个方法,表示这个方法中的所有代码都需要线程同步。

synchronized关键字修饰非静态方法,默认使用this当做锁对象,并且不能自己另外指定

synchronized关键字修饰静态方法,默认使用当前类的Class对象当做锁对象,并且不能自己另外指定

这俩中情况的同步效果是一样的,只是锁对象不同而已

例如,

public class MyData{
    private int[] arr = new int[20];
    //当前数据可以存放的位置,也表示当前存放的元素个数
    private int current;

    //添加数据
    public void add(int num){
        String name = Thread.currentThread().getName();
        arr[current] = num;
        System.out.println(name+"线程本次写入的值为"+num+",写入后取出的值为"+arr[current]);

        current++;
    }

}

注意,MyData类中的add方法,在多线程并发访问的环境中,是有线程安全问题的

例如,

public class Test {

    public static void main(String[] args) {

        MyData myData = new MyData();

        Thread t1 = new Thread("t1"){
            @Override
            public void run() {
                for (int i = 0; i < 10; i++) {
                    myData.add(i);
                    //计算机运行10次运行太快了,让它执行慢一些,好观察效果
                    try {
                        Thread.sleep(1);
                    } catch (InterruptedException e) {
                        e.printStackTrace();
                    }
                }
            }
        };

        //t2线程的名字前面加个制表符\t,打印的时候好观察
        Thread t2 = new Thread("\tt2"){
            @Override
            public void run() {
                for (int i = 10; i < 20; i++) {
                    myData.add(i);
                    //计算机运行10次运行太快了,让它执行慢一些,好观察效果
                    try {
                        Thread.sleep(1);
                    } catch (InterruptedException e) {
                        e.printStackTrace();
                    }
                }
            }
        };

        t1.start();
        t2.start();


    }

}

//运行结果:
t1线程本次写入的值为0,写入后取出的值为0
	t2线程本次写入的值为10,写入后取出的值为10
	t2线程本次写入的值为11,写入后取出的值为1
t1线程本次写入的值为1,写入后取出的值为1
	t2线程本次写入的值为12,写入后取出的值为2
t1线程本次写入的值为2,写入后取出的值为2
	t2线程本次写入的值为13,写入后取出的值为3
t1线程本次写入的值为3,写入后取出的值为3
	t2线程本次写入的值为14,写入后取出的值为4
t1线程本次写入的值为4,写入后取出的值为4
t1线程本次写入的值为5,写入后取出的值为5
	t2线程本次写入的值为15,写入后取出的值为5
	t2线程本次写入的值为16,写入后取出的值为16
t1线程本次写入的值为6,写入后取出的值为16
t1线程本次写入的值为7,写入后取出的值为17
	t2线程本次写入的值为17,写入后取出的值为17
t1线程本次写入的值为8,写入后取出的值为18
	t2线程本次写入的值为18,写入后取出的值为18
t1线程本次写入的值为9,写入后取出的值为19
	t2线程本次写入的值为19,写入后取出的值为19

可以看出,在某一次add方法执行的时候,会出现写入的数据和当前的数据不一致的情况

此时,我们可以直接在add方法(非静态方法)上,添加修饰符synchronized关键字,表示给这个方法中的所有代码进行线程同步,默认使用的锁对象是this

public synchronized void add(int num){
    String name = Thread.currentThread().getName();
    arr[current] = num;
    System.out.println(name+"线程本次写入的值为"+num+",写入后取出的值为"+arr[current]);

    current++;
}

此时,再运行代码,就不会出现之前那种线程安全问题了

该代码表示,拿到锁对象this的线程,才可以进入到add方法中执行代码,代码执行完,会释放锁,这时锁变的可用了,所有需要这把锁的线程都恢复到RUNABLE状态(它们之前在锁阻塞状态),这些线程一起重新争夺CPU执行权,谁先拿到CPU执行权,就会先过去拿到锁,进入代码去执行

注意,此时t1线程中调用add方法,争夺的锁对象this就是myData对象,t2线程中调用的add方法,争夺的锁对象this也是myData对象,所以t1和t2俩个线程争夺的是同一把锁对象,那么就能达到线程同步的效果!

所以,线程同步的效果的关键点在于,让t1和t2俩个线程去争夺同一把锁对象

思考,根据上面的例子,考虑为什么之前学习的ArrayList中的add方法是非线程安全的,而Vector中的add方法是线程安全的?

7 wait和notify

Object类中有三个方法:wait()、notify()、notifyAll

当一个对象,在线程同步的代码中,充当锁对象的时候,在synchronized同步的代码块中,就可以调用这个锁对象的这三个方法了。

三个核心点:

  • 任何对象中都一定有这三个方法
  • 只有对象作为锁对象的时候,才可以调用
  • 只有在同步的代码块中,才可以调用

其他情况下,调用一个对象的这三个方法,都会报错!

synchronized关键字,虽然可以达到线程同步的效果,但是太“霸道”了,只要一个线程拿到了锁对象,那么这个线程无论是在运行状态,还是时间片用完,回到就绪状态,还是sleep休眠,这个线程都是死死的拿着这个锁对象不释放,只有这个线程把线程同步的代码执行完,才会释放锁对象让别的线程使用。

那么有没有一个方法,可以让拿到的锁的线程,即使代码没执行完,也可以把锁立即给释放了呢?

有的,这个就是wait方法

例如,

public class Test {

    public static void main(String[] args) {

        final Object obj = new Object();

        Thread t1 = new Thread("t1"){
            @Override
            public void run() {
                String name = Thread.currentThread().getName();
                synchronized (obj){
                    for (int i = 0; i < 10; i++) {
                        System.out.println(name+"线程: i = "+i);
                    }
                }
            }
        };

        Thread t2 = new Thread("t2"){
            @Override
            public void run() {
                String name = Thread.currentThread().getName();
                synchronized (obj){
                    for (int j = 10; j < 20; j++) {
                        System.out.println(name+"线程: j = "+j);
                    }
                }
            }
        };

        t1.start();
        t2.start();


    }

}

t1和t2俩个线程,争夺同一把锁对象obj,所以程序的运行结果是:要么t1先拿到锁输处09,然后t2再拿到锁输出1019,要么就是t2先拿到锁输入1019,然后t1再拿到锁输出09

现在,我们希望的是t1线程中i=5的时候,先释放锁,让t2拿到锁去运行,在t2线程中,当j=15的时候,释放锁,让t1拿到锁去运行:

在代码中加入条件判断和wait方法的调用

public class Test {

    public static void main(String[] args) {

        final Object obj = new Object();

        Thread t1 = new Thread("t1"){
            @Override
            public void run() {
                String name = Thread.currentThread().getName();
                synchronized (obj){
                    for (int i = 0; i < 10; i++) {
                        System.out.println(name+"线程: i = "+i);
                        if(i==5){
                            try {
                                //obj是所调用,在同步代码块中,可以调用wait方法
                                //让当前拿到锁的线程,立即释放锁
                                obj.wait();
                            } catch (InterruptedException e) {
                                e.printStackTrace();
                            }
                        }
                    }
                }
            }
        };

        Thread t2 = new Thread("t2"){
            @Override
            public void run() {
                String name = Thread.currentThread().getName();
                synchronized (obj){
                    for (int j = 10; j < 20; j++) {
                        System.out.println(name+"线程: j = "+j);
                        if(j==15){
                            try {
                                //obj是所调用,在同步代码块中,可以调用wait方法
                                //让当前拿到锁的线程,立即释放锁
                                obj.wait();
                            } catch (InterruptedException e) {
                                e.printStackTrace();
                            }
                        }
                    }
                }
            }
        };

        t1.start();
        t2.start();


    }

}

//运行结果:
t1线程: i = 0
t1线程: i = 1
t1线程: i = 2
t1线程: i = 3
t1线程: i = 4
t1线程: i = 5
t2线程: j = 10
t2线程: j = 11
t2线程: j = 12
t2线程: j = 13
t2线程: j = 14
t2线程: j = 15

可以看到,t1线程和t2线程都没有运行完,但是代码不运行了,JVM也没停住

这是因为,当前调用锁对象的wait方法后,当前线程释放锁,然后进入到阻塞状态,并且等待其他线程先唤醒自己,如果没有其他线程唤醒自己,那么就一直等着。所以现在的情况是,俩个线程t1和t2都是在处于阻塞状态,等待别人唤醒自己,所以程序不运行了,但是也没结束!

此时,调用t1和t2的getState方法,返回的状态为:WAITING

public class Test {

    public static void main(String[] args) {

        final Object obj = new Object();

        Thread t1 = new Thread("t1"){
            @Override
            public void run() {
                String name = Thread.currentThread().getName();
                synchronized (obj){
                    for (int i = 0; i < 10; i++) {
                        System.out.println(name+"线程: i = "+i);
                        if(i==5){
                            try {
                                obj.wait();
                            } catch (InterruptedException e) {
                                e.printStackTrace();
                            }
                        }
                    }
                }
            }
        };

        Thread t2 = new Thread("t2"){
            @Override
            public void run() {
                String name = Thread.currentThread().getName();
                synchronized (obj){
                    for (int j = 10; j < 20; j++) {
                        System.out.println(name+"线程: j = "+j);
                        if(j==15){
                            try {
                                obj.wait();
                            } catch (InterruptedException e) {
                                e.printStackTrace();
                            }
                        }
                    }
                }
            }
        };

        t1.start();
        t2.start();

        //主线程休眠1秒钟,给t1和t2点时间,等它们调用wait方法
        try {
            Thread.sleep(1000);
        } catch (InterruptedException e) {
            e.printStackTrace();
        }

        System.out.println("t1线程当前的状态为:"+t1.getState());
        System.out.println("t2线程当前的状态为:"+t2.getState());


    }

}

//运行结果:
t1线程: i = 0
t1线程: i = 1
t1线程: i = 2
t1线程: i = 3
t1线程: i = 4
t1线程: i = 5
t2线程: j = 10
t2线程: j = 11
t2线程: j = 12
t2线程: j = 13
t2线程: j = 14
t2线程: j = 15
t1线程当前的状态为:WAITING
t2线程当前的状态为:WAITING

此时线程的状态图为:

在这里插入图片描述

可以看出,此时线程调用了wait方法,释放了锁,变为阻塞状态(WAITING),并进入了等待池,等待其他线程唤醒自己或者打断自己,如果有线程调用了notify方法进行了唤醒,或者interrupt方法进行了打断,那么这个线程就会从等待池进入到锁池,而进入到锁池的线程,会时刻关注锁对象是否可用,一旦可用,这个线程就会立刻自动恢复到RUNNABLE状态。

由图可知,TIMED_WAITING、WAITING、BLOCKED都属于线程阻塞,他们共同的特点是就是线程不执行代码,也不参与CPU的争夺,除此之外,它们还有各自的特点:(重要)

  • 阻塞1,线程运行时,调用sleep或者join方法后,进入这种阻塞,该阻塞状态可以恢复到RUNNABLE状态,条件是线程被打断了、或者指定的时间到了,或者join的线程结束了
  • 阻塞2,线程运行时,发现锁不可用后,进入这种阻塞,该阻塞状态可以恢复到RUNNABLE状态,条件是线程需要争夺的锁对象变为可用了(别的线程把锁释放了)
  • 阻塞3,线程运行时,调用了wait方法后,线程先释放锁后,再进入这种阻塞,该阻塞状态可以恢复到BLOCKED状态(也就是阻塞2的情况),条件是线程被打断了、或者是被别的线程唤醒了(notify方法)

理解上述的状态变化过程后,我们修改代码,加入notify方法的调用:

public class Test {

    public static void main(String[] args) {

        final Object obj = new Object();

        Thread t1 = new Thread("t1"){
            @Override
            public void run() {
                String name = Thread.currentThread().getName();
                synchronized (obj){
                    for (int i = 0; i < 10; i++) {
                        System.out.println(name+"线程: i = "+i);
                        if(i==5){
                            try {
                                //在释放锁对象之前,叫醒等待池中等待obj锁对象的线程
                                //意思是告诉对方,我要释放锁了,你准备去抢把
                                obj.notify();
                                obj.wait();
                            } catch (InterruptedException e) {
                                e.printStackTrace();
                            }
                        }
                    }

                    //最后在执行完所有代码之前,在叫醒一次,防止等待池中还线程在等待obj这个锁对象
                    obj.notify();

                }
            }
        };

        Thread t2 = new Thread("t2"){
            @Override
            public void run() {
                String name = Thread.currentThread().getName();
                synchronized (obj){
                    for (int j = 10; j < 20; j++) {
                        System.out.println(name+"线程: j = "+j);
                        if(j==15){
                            try {
                                //在释放锁对象之前,叫醒等待池中等待obj锁对象的线程
                                //意思是告诉对方,我要释放锁了,你准备去抢把
                                obj.notify();
                                obj.wait();
                            } catch (InterruptedException e) {
                                e.printStackTrace();
                            }
                        }
                    }

                    //最后在执行完所有代码之前,在叫醒一次,防止等待池中还线程在等待obj这个锁对象
                    obj.notify();

                }
            }
        };

        t1.start();
        t2.start();

        //主线程休眠1秒钟,给t1和t2点时间,等它们调用wait方法
        try {
            Thread.sleep(1000);
        } catch (InterruptedException e) {
            e.printStackTrace();
        }

        System.out.println("t1线程当前的状态为:"+t1.getState());
        System.out.println("t2线程当前的状态为:"+t2.getState());


    }

}
//运行结果:
t1线程: i = 0
t1线程: i = 1
t1线程: i = 2
t1线程: i = 3
t1线程: i = 4
t1线程: i = 5
t2线程: j = 10
t2线程: j = 11
t2线程: j = 12
t2线程: j = 13
t2线程: j = 14
t2线程: j = 15
t1线程: i = 6
t1线程: i = 7
t1线程: i = 8
t1线程: i = 9
t2线程: j = 16
t2线程: j = 17
t2线程: j = 18
t2线程: j = 19
t1线程当前的状态为:TERMINATED
t2线程当前的状态为:TERMINATED


可以看到,此时t1和t2俩个线程都执行完了,打印输出的结果也符合我们的预期

锁对象.notify(),该方法可以在等待池中,随机唤醒一个等待指定锁对象的线程,使得这个线程进入到锁池中,而进入到锁池的线程, 一旦发现锁可用,就可以自动恢复到RUNNABLE状态了

锁对象.notifyAll(),该方法可以在等待池中,唤醒所有等待指定锁对象的线程,使得这个线程进入到锁池中,而进入到锁池的线程, 一旦发现锁可用,就可以自动恢复到RUNNABLE状态了

思考,线程调用无参的wait方法,会释放锁并进入等待池,且此时的状态为WAITING,如果线程调用有参的wait方法,指定一个等待时间,那么线程释放锁后,进入到等待池,此时的状态还是WAITING么?

那就不是的,如果指定了一个等待时间,那状态就是timed_waiting了,这两者的区别就是waiting是一定要通过别的方法唤醒它才能进入就绪状态,而timed_waiting只要时间到了自动变为就绪状态同其他线程争取时间片。

案例:

根据以下代码,补全pos和wit方法完成其功能

要求,银行账号的余额不能是负数

public class Account {
    //账号余额
    private int balance;
    public Account(int balnace) {
        this.balance = balnace;
    }
    //存钱
    public void pos(int money){

    }
    //消费
    public void wit(int money){

    }
}
//男孩,负责挣钱
public class Boy extends Thread{
    private Account account;
    public Boy(Account account, String name) {
        this.account = account;
        setName(name);
    }
    public void run() {
        //一直不停的挣钱
        while(true){
            //随机钱数
            int money = (int)(Math.random()*10000+1);
            //存进账户
            account.pos(money);
            try {
                //休息1秒后,继续挣钱
                Thread.sleep(1000);
            } catch (InterruptedException e) {
                e.printStackTrace();
            }
        }
    }

}
//女孩,负责花钱
public class Girl extends Thread{
    private Account account;
    public Girl(Account account, String name) {
        this.account = account;
        setName(name);
    }
    public void run() {
        //一直不停的花钱
        while(true){
            //随机钱数(看心情)
            int money = (int)(Math.random()*10000+1);
            //刷卡消费
            account.wit(money);
            try {
                //休息1秒后,继续花钱
                Thread.sleep(1000);
            } catch (InterruptedException e) {
                e.printStackTrace();
            }
        }
    }
}
public class AccountTest {
    public static void main(String[] args) {
        Account account = new Account(5000);
        Boy boy = new Boy(account, "tom");
        Girl lily1 = new Girl(account, "lily1");
        Girl lily2 = new Girl(account, "lily2");
        Girl lily3 = new Girl(account, "lily3");

        boy.start();
        lily1.start();
        lily2.start();
        lily3.start();
    }
}

8 死锁

在程序中要尽量避免出现死锁情况,一旦发生那么只能手动停止JVM的运行,然后查找并修改产生死锁的问题代码

简单的描述死锁就是:俩个线程t1和t2,t1拿着t2需要等待的锁不释放,而t2又拿着t1需要等待的锁不释放,俩个线程就这样一直僵持下去。

例如,

public class ThreadDeadLock extends Thread{
    private Object obj1;
    private Object obj2;

    public ThreadDeadLock(Object obj1,Object obj2) {
        this.obj1 = obj1;
        this.obj2 = obj2;
    }

    public void run() {
        String name = Thread.currentThread().getName();
        if("Thread-0".equals(name)){
            while(true){
                synchronized (obj1) {
                    synchronized (obj2) {
                        System.out.println(name+" 运行了..");
                    }
                }
            }
        }
        else{
            while(true){
                synchronized (obj2) {
                    synchronized (obj1) {
                        System.out.println(name+" 运行了..");
                    }
                }
            }
        }
    }

    public static void main(String[] args) {
        Object obj1 = new Object();
        Object obj2 = new Object();
        Thread t1 = new ThreadDeadLock(obj1,obj2);
        Thread t2 = new ThreadDeadLock(obj1,obj2);
        t1.start();
        t2.start();
    }
}

注意,可以通过jconsole查看到线程死锁的情况

  • 0
    点赞
  • 2
    收藏
    觉得还不错? 一键收藏
  • 0
    评论

“相关推荐”对你有帮助么?

  • 非常没帮助
  • 没帮助
  • 一般
  • 有帮助
  • 非常有帮助
提交
评论
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

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

抵扣说明:

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

余额充值