Thread & Concurrency

Process & Thread

计算机上有很多进程,一个进程被分配多个线程来处理各种各样的任务

但是我们想到一个问题,这些任务还是在一台计算机上运行的,那么关于存取的数据也都来自一台计算机,如果没有达到理想中的安全操作,那么结果一定不是我们所想象的。就比如一个线程给一个变量赋予了新值,但另一个线程没有及时读取到依然使用着旧值。

于是提出了并发编程的概念 -- Concurrent Programming

他需要满足三个条件:Atomicity, Visibility, Order of execution

原子性:一个操作无法被打断,也就是一但开始就一定会持续到结束

可见性:当多个线程访问同一个变量时,一个线程修改了这个变量的值,其他线程立刻可以看到这个修改值

有序性:程序执行顺序必须按照代码先后执行

这样单纯的看三个概念比较空,大概知道意思即可,而且通过描述不难看出,这是关于多线程的操作,如果是单一线程操作的话是不需要考虑并发的。总之这些概念后续还会提到。

线程简述

我们先来看一下API,总结一下就是你的程序会有一个主线程(Thread main);线程之间有Priority(优先级);有User Thread(用户线程)、Daemon Thread(守护线程) 等等。

这些可以都不用管继续往下看

线程有6种状态

1.初始(NEW):新创建了一个线程对象,但还没有调用start()方法时的状态。

2.运行(RUNNABLE):Java线程中将操作系统中的就绪(ready)和运行中(running)两种状态笼统的称为“可运行”RUNNABLE状态。
线程对象创建后,其他线程(比如main线程)调用了该对象的start()方法。该状态的线程位于可运行线程池中,等待被线程调度选中,获取CPU的使用权,此时处于就绪状态(ready)。就绪状态的线程在获得CPU时间片后变为运行中状态(running)。所以调用了start()方法并不意味这线程会立即执行。

3.阻塞(BLOCKED):表示线程阻塞于锁。仅在Synchronized代码块中,且没有获得锁的状态。
等待的是其他线程释放排他锁。

4.等待(WAITING):进入该状态的线程需要等待其他线程做出一些特定动作(通知或中断)。
无期限等待。等待被唤醒。

5.超时等待(TIMED_WAITING):该状态不同于WAITING,它可以在指定的时间后自行返回。
有时间期限的等待。等待一段设置好的时间。

6.终止(TERMINATED):表示该线程已经执行完毕。
线程正常的执行完毕,会进入到这种状态。或者是run()方法中出现了异常,线程会意外终止。
 

不同的状态之下对应着不同的线程操作及方法。

线程的创建方式

两种的主要区别在于怎么new线程。

注意new出来后,线程处于New状态。

调用线程的start()方法后,线程进入Runnable状态。但此时只是可运行状态中的就绪状态。

当线程的run()方法被调用后,这是才是可运行状态的运行状态。后续会详说。

extends Thread

public class MyThread extends Thread{
    public void run(){
        //some action
    }
}

public class Test {
    public static void main(String[] args){
        MyThread thread=new MyThread();
        thread.start();
    }
}

implements Runnable

public class MyRunnable implements Runnable{
    public void run(){
        //some action
    }
}

public class Test {
    public static void main(String[] args){
        MyRunnable runnable=new MyRunnable();
        Thread thread=new Thread(runnable);
        thread.start();
    }
}

推荐implements Runnable ,因为接口可以实现多个,父类只能继承一个,在现实开发中肯定不会只有一个接口的。

线程运行结果的随机性

为什么调用start执行的是run? 

其实Runnable之下又分两种状态:就绪状态,运行中状态

就绪状态与运行中状态

就绪状态(Ready)只是表名线程有资格运行,若调度程序没有挑选到此线程,此线程将永远是就绪状态。当调度程序挑选中该线程,此线程就将进入运行中状态(Running)

拿实例来分析一下就懂了

我们new出来后,进入New状态,然后调用start方法,才会进入runnale状态。但是现在都是处于就绪状态。

假设我现在有三个线程在可运行状态的就绪状态,这时CPU就会随机选取一个为其分配时间片

比如选择了3线程,3就进入可运行状态的运行中状态,然后就开时执行3的run方法体内的action

等时间片结束后,线程又有回到Runnable状态(的就绪状态),然后继续随机选取线程

一直重复这个过程直到所有线程运行完毕。至于时间片是什么,你可以理解为运行多少行代码。

这种随机分配时间片的方式导致了线程运行结果的随机性。

 比如你看这段代码:

class MyRunnable implements Runnable{
    public void run(){
        for(int i=1;i<=5;i++){
            System.out.println(Thread.currentThread().getName()+" "+i);
        }
    }
}

public class Test {
    public static void main(String[] args){
        MyRunnable runnable=new MyRunnable();
        Thread thread1=new Thread(runnable);
        Thread thread2=new Thread(runnable);
        thread1.start();
        thread2.start();
    }
}

 我随便运行两次看一下结果

很明显的不一样对吧,两个线程run方法是交错运行的。

我们来分析一下

在main方法里,我们先new了两个线程,这时还没调用start方法。

这时处于New状态的就绪状态的线程就有:Thread-0,Thread-1

处于Runnable状态的线程有:Thread-main

Runnable里面只有 Thread-main,所以CPU选中 Thread-main 分配时间片,使其从就绪状态进入运行状态。

Thread-main 将运行两个start方法。这样Runnable就有了三个线程

等 Thread-main 的时间片结束后,Thread-main 又回到就绪状态。

CPU再次随机选择一个出来运行,比如我们看第二张图的运行结果。

选择到了Thread-1,时间片只有1行代码,打印出来一行就回到就绪状态。

然后选择到了Thread-0 ,时间片有4行代码,打印出来四行就回到就绪状态。

然后又是Thread-1

又是Thread-0

...

如果看懂这个意思的话,就可以大概理解为什么运行结果是随机的。但其实实际可能比我分析的要复杂的多,比如你看到连续打印出Thread-0的四行,可能是连续四次选中Thread-0,每次打印一行。

不光如此,还有Thread-main,我这里只是假设它在一开始的时间片就运行到了thread2.start() 这行结束,但是也可能先运行到thread1.start() 这里结束,细细分析的话这些造成的结果都不会一样的。

在这些线程优先级一样的情况下(构造方法中默认都是5好像,范围是从1到10),这个随机过程需要理解,不然比较复杂。

同时我们要知道,这种结果的随机性,是线程不安全的。为什么后续会说。

synchronized关键字

上面的内容让我们看到,当我们进行多线程操作时,结果的随机性无法满足我们的现实需求。那么如何控制这种随机性?也就是让程序变得线程安全。

如果我们在运行多个线程的时候,锁定一个线程运行,也就是先把这个线程运行完再去管别的线程。那么结果就不会是随机的而是受我们控制的。

线程锁可以让我们实现这个目标。锁分为两种 :对象锁,类锁。锁这种操作我们通过关键字synchronized来实现,注意拼写

锁的简介

我们先来看个简单的例子,我们在上面那个例子中加入了synchronized代码块,将我们run方法中的action包裹住。

class MyRunnable implements Runnable{
    public void run(){
        synchronized(this){
            for(int i=1;i<=5;i++){
                System.out.println(Thread.currentThread().getName()+" "+i);
            }
            System.out.println(Thread.currentThread().getName()+" Finish!");
        }
    }

}

public class Test {
    public static void main(String[] args){
        MyRunnable runnable=new MyRunnable();
        Thread thread1=new Thread(runnable);
        Thread thread2=new Thread(runnable);
        thread1.start();
        thread2.start();
    }
}

无论你怎么运行,结果只有这一种可能,不会再出现随机性。

这里我存一点疑,回头再查查。就是按理来说,thread1和thread2谁先被调度中心选中应该是随机的。那么下面的结果中,也有可能先打印Thread-1 然后再Thread-0。但是我试了很多遍都是一样的结果。回头我再研究一下。

分析一下(我们暂时不管Thread-main,单纯分析Thread-0和Thread-1)

我们要理解一下synchronized(this),里的这个this

当thread1.start()后

thread1对应了 Thread thread1=new Thread(runnable)

而runnable对应着MyRunnable runnable=new MyRunnable()

而synchronized(this)写在MyRunnable类里面,那么这个this就指代着MyRunnable这个类下的对象

所以当thread1的run方法被调用后,thread1的runnable被synchronized(this)的this承接,也就是说thread1获取了这个锁。

而对于thread2来说,它的runnable和thread1的runnable是同一个。

但是这个runnable已经被thread1调到synchronized(this)这个锁中,那么thread2在这个锁释放之前就不能用runnable。

所以会一直等到thread1的action全部执行完毕,才能让thread2获取锁并执行它的代码。

这个过程中thread1可能会没运行完代码,时间片就结束回到调度中心。但是这时候这个锁是thread1的,就算调度中心选中了thread2,它也会立刻回到调度中心,只有选中thread1才会继续操作。

你可以理解为这个时间段,MyRunnable这个类跟thread1锁在一起,其他线程无法调用。

如果分析没看懂的话,在看下面这段代码

class MyRunnable implements Runnable{
    public void run(){
        synchronized(this){
            for(int i=1;i<=5;i++){
                System.out.println(Thread.currentThread().getName()+" "+i);
            }
            System.out.println(Thread.currentThread().getName()+" Finish!");
        }
    }

}

public class Test {
    public static void main(String[] args){
        MyRunnable runnable1=new MyRunnable();
        MyRunnable runnable2=new MyRunnable();
        Thread thread1=new Thread(runnable1);
        Thread thread2=new Thread(runnable2);

        thread1.start();
        thread2.start();


    }
}

我们让thread1和thread2分别对应runnable1和runnable2。你可以理解为有了两个锁,分别对应两个线程,它们之间没有关系互不干扰,那么结果就会交错了。相当于我们谁都没上锁:

那么如果我们这样写呢,run方法体内部有一部分被synchronized套住,有一部分没被套住。那么对于这部分套住的,依然遵从锁的规则,而没被套住的仍是那种随机交错的打印。

class MyRunnable implements Runnable{
    public void run(){
        synchronized(this){
            for(int i=1;i<=5;i++){
                System.out.println(Thread.currentThread().getName()+" "+i+" - Block");
            }
        }
        for(int j=1;j<=5;j++) {
            System.out.println(Thread.currentThread().getName()+" "+j+" - unBlock");
        }
    }

}

public class Test {
    public static void main(String[] args){
        MyRunnable runnable=new MyRunnable();
        Thread thread1=new Thread(runnable);
        Thread thread2=new Thread(runnable);

        thread1.start();
        thread2.start();
    }
}

我们来看结果,这个例子可能不太明显,但目的就是展现这种synchronized套住的部分才是上锁

总之通过这些简单的介绍,大概理解锁的作用,怎么用代码实现。代码的意义,以及获取锁的对象(也就是谁被上锁),这个很重要!

对象锁

啥叫对象锁呢,具体来说就是,对一个对象上锁。也就是获取锁的是一个对象。

就是我们所举的例子,这个this指代了一个MyRunnable对象:

class MyRunnable implements Runnable{
    public void run(){
        synchronized(this){
            for(int i=1;i<=5;i++){
                System.out.println(Thread.currentThread().getName()+" "+i);
            }
            System.out.println(Thread.currentThread().getName()+" Finish!");
        }
    }

}

public class Test {
    public static void main(String[] args){
        MyRunnable runnable=new MyRunnable();
        Thread thread1=new Thread(runnable);
        Thread thread2=new Thread(runnable);
        thread1.start();
        thread2.start();
    }
}

它还有一种等价写法,用synchronized修饰run方法:

class MyRunnable implements Runnable{
    public synchronized void run(){
        for(int i=1;i<=5;i++){
            System.out.println(Thread.currentThread().getName()+" "+i+" - Block");
        }
    }
}

public class Test {
    public static void main(String[] args){
        MyRunnable runnable=new MyRunnable();
        Thread thread1=new Thread(runnable);
        Thread thread2=new Thread(runnable);

        thread1.start();
        thread2.start();
    }
}

也就是说,下面这两种形式是等价的,它们都是让一个对象获取这个锁:


//method 1
public synchronized void method()
{
   // todo
}


//method 2
public void method()
{
   synchronized(this) {
      
   }
}

类锁

类锁可以被一个类下面所有的对象获取。也就是说,只要你是这个类,那么你都可以获取这个锁。

我们来看例子,刚刚上面写了一个对象锁,但是因为是两个对象,相当于两个锁互不干扰,所以结果相当于没有上锁,结果是交错的。

class MyRunnable implements Runnable{
    public void run(){
        synchronized(this){
            for(int i=1;i<=5;i++){
                System.out.println(Thread.currentThread().getName()+" "+i);
            }
            System.out.println(Thread.currentThread().getName()+" Finish!");
        }
    }

}

public class Test {
    public static void main(String[] args){
        MyRunnable runnable1=new MyRunnable();
        MyRunnable runnable2=new MyRunnable();
        Thread thread1=new Thread(runnable1);
        Thread thread2=new Thread(runnable2);

        thread1.start();
        thread2.start();


    }
}

我们把它转化为类锁,如下,我们在synchronized里面改成MyRunnable.class。那么无论是runnable1还是runnable2,都可以获取这个锁。那么他的结果必然是分开的。

class MyRunnable implements Runnable{
    public void run(){
        synchronized(MyRunnable.class){
            for(int i=1;i<=5;i++){
                System.out.println(Thread.currentThread().getName()+" "+i);
            }
            System.out.println(Thread.currentThread().getName()+" Finish!");
        }
    }

}

public class Test {
    public static void main(String[] args){
        MyRunnable runnable1=new MyRunnable();
        MyRunnable runnable2=new MyRunnable();
        Thread thread1=new Thread(runnable1);
        Thread thread2=new Thread(runnable2);

        thread1.start();
        thread2.start();
    }
}

结果如下,首先runnable1获取这个类锁并执行它的代码,runnable2必须等runnable1执行完将锁释放后,才能获取这个锁,然后执行代码。所以结果又是这样分开的。

 类锁也有一种等价形式,它是用synchronized修饰静态方法。因为静态方法属于类而不是对象。写法如下,我们在run方法内调用一个synchronized修饰的静态方法。为啥不直接写给run,因为run是接口内的方法无法被定义为静态方法。

class MyRunnable implements Runnable{
    public void run(){
        method();
    }

    public synchronized static void method(){
        for(int i=1;i<=5;i++){
            System.out.println(Thread.currentThread().getName()+" "+i);
        }
        System.out.println(Thread.currentThread().getName()+" Finish!");
    }
}

public class Test {
    public static void main(String[] args){
        MyRunnable runnable1=new MyRunnable();
        MyRunnable runnable2=new MyRunnable();
        Thread thread1=new Thread(runnable1);
        Thread thread2=new Thread(runnable2);

        thread1.start();
        thread2.start();
    }
}

也就是说,下面两种写法是等价的,它们都是让一个类的所以对象获取这个锁

//method 1
class MyRunnable implements Runnable{
    public void run(){
        method();
    }

    public synchronized static void method(){
        //todo
    }
}

//method 2
class MyRunnable implements Runnable{
    public void run(){
        synchronized(MyRunnable.class){
            //todo
        }
    }

}

关于锁的更详细说明

可能刚开始接触都有一点懵。但是我们需要清楚一段代码中,它有几个锁,这些锁是类锁还是对象锁,它们分别对那个类和对象起作用(也就是谁可以获取这个锁)

注意,类锁与对象锁之间是没有关系的

我们来总结一下

1. 对象锁

· 当使用对象锁的时候,注意要是相同的对象,并且当有线程正在访问对象锁内部的代码的时候,其他线程无法访问。(注意无法访问的范围)。

· 但是并不影响没有使用对象锁的部分的代码的运行。

· 对象锁分为两类一个叫做synchronized代码块(圆括号内是普通类的对象),另外一个是synchronized修饰普通成员方法。(见我前面给的等价例子)。它们二者其实可以通过this关键字相互转化。

2. 类锁

· 当使用类锁的时候,只要是同一个类的对象就可以获取锁。当有线程正在访问类锁内部的代码的时候,其他线程无法访问。(注意无法访问的范围)

· 但是并不影响没有使用类锁的部分的代码的运行

· 对象锁分为两类一个叫做synchronized代码块(圆括号内是class对象),另外一个是synchronized修饰静态成员方法。它们二者其实可以通过class对象互相转化。

注意,类锁与对象锁之间是没有关系的

而且,也不是一定将Runnable这个类当成锁(像我上面给出的例子那样),我们同样可以自己定义一个类来当锁。所以关键是在于,这个锁是让谁来获取的。

class Block{
    int ID;

    public Block(int ID){
        this.ID=ID;
    }
}

class MyRunnable implements Runnable{
    Block block;

    void setBlock(Block block){
        this.block=block;
    }

    public void run(){
        synchronized(block){
            System.out.println(Thread.currentThread().getName());
        }
    }
}

public class Test {
    public static void main(String[] args){

        MyRunnable runnable=new MyRunnable();
        runnable.setBlock(new Block(1));

        Thread thread1=new Thread(runnable);
        Thread thread2=new Thread(runnable);

        thread1.start();
        thread2.start();
    }
}

关于锁的知识,大概明白这些就ok了。但这些只是基础,现实会更加综合一点。比如可以把一部分上类锁,一部分上对象锁,一部分不上锁。把这些堆在一起就较为复杂了。

但是还是要强调一下,类锁和对象锁之间没有关系!

线程的控制

这里就将涉及到一些Thread类中的方法,它们将会改变线程的状态。达到一些特殊的效果,让我们进一步控制线程。也就是让线程状态来回转换。我们先梳理一下线程的状态。

线程状态关系图

我们先来看这张图,我们上面已经讲过了 new runnable running blocked 状态。大家应该可以对应上。这里我们就要开始下面的 waiting 和 timed-waiting。其实很多教程也把等待叫成阻塞状态。我也比较习惯说成阻塞,但是大体意思都是易懂的。就是后面有些地方我可能写成阻塞了,但理解我的意思就好。

图中的几个方法我们并不涉及自行略过就好。

然后这里的runnable其实就代表着我们上面提到的runnable下的就绪(ready)状态,就是我常说的调度中心。我们可以看到这些关于线程的控制方法,会让他们进入等待状态,然后等待状态在到达条件后回到调度中心。这个调度中心,我们上面已经很清楚的解释里面发生的事情。也就是线程的随机性结果。

所以在控制线程的同时,线程的运行结果依然随机。

static回顾

这些东西是我基本学完线程后来写的,所以在开始后面的内容之前觉得应该强调一些东西。

Thread这个类中有很多静态方法,比如currentThread,sleep等等等等

啥是静态方法呢?

在类中使用static修饰的静态方法会随着类的定义而被分配和装载入内存中;而非静态方法属于对象的具体实例,只有在类的对象创建时在对象的内存中才有这个方法的代码段。

静态方法是属于类的,并不属于一个实例对象。

注意: 非静态方法既可以访问静态数据成员 又可以访问非静态数据成员,而静态方法只能访问静态数据成员;
非静态方法既可以访问静态方法又可以访问非静态方法,而静态方法只能访问静态数据方法。

原因: 因为静态方法和静态数据成员会随着类的定义而被分配和装载入内存中,而非静态方法和非静态数据成员只有在类的对象创建时在对象的内存中才有这个方法的代码段。

引用静态方法时,可以用 类名.方法名 或者 对象名.方法名 的形式

sleep() 

回顾完static,我们先来看sleep方法。这就是一个静态方法。直接看例子:

class MyRunnable implements Runnable{
    public void run(){
        for(int i=1;i<=5;i++){
            System.out.println(Thread.currentThread().getName()+" "+i);
            if(i==2){
                try {
                    System.out.println("Sleeping...");
                    Thread.sleep(1000);
                } catch (InterruptedException e) {
                    e.printStackTrace();
                }
            }
        }
    }
}

public class Test {
    public static void main(String[] args){
        MyRunnable runnable=new MyRunnable();
        Thread thread=new Thread(runnable);

        thread.start();

    }
}

结果如你所想,会在打印Sleeping...停顿1000ms然后继续运行:

sleep的作用就是 会让让前线程进入阻塞状态,但是不会释放锁。注意,它会让线程丢弃当前剩余的时间片,立马进入阻塞状态。同时sleep会抛出异常需要处理一下。

不会释放锁的意思结合下上面讲的关于锁的,也就是这个线程拿到锁后,它睡完后这个锁依然在它手上直到它的代码运行完毕才会释放锁。这里就不放例子了。

但是有一个问题。这里写的是Thread.sleep(),我们采用类名.方法名来调用静态方法。

那么哪个线程进入阻塞?(这种分析在后续几个方法中都会出现,所以只以sleep为例讲一遍)

API中是这么叙述的: " Causes the currently executing thread to sleep (temporarily cease execution) for the specified number of milliseconds. "

也就是当前运行的线程,可能不太好理解。我们来看上面给的例子,Thread.sleep()写在run方法体内。那么如果运行到这,当前运行的线程就是正在调用run()的线程。

这里调度中心选中了唯一存在的Thread0,执行它的run方法,那么自然就是Thread0睡眠。

还是有点乱,继续来看:

class MyRunnable implements Runnable{
    public void run(){
        for(int i=1;i<=5;i++){
            System.out.println(Thread.currentThread().getName()+" "+i);
            if(i==2){
                try {
                    System.out.println("Sleeping...");
                    Thread.sleep(1000);
                } catch (InterruptedException e) {
                    e.printStackTrace();
                }
            }
        }
    }
}

public class Test {
    public static void main(String[] args){
        MyRunnable runnable=new MyRunnable();
        Thread thread0=new Thread(runnable);
        Thread thread1=new Thread(runnable);

        thread0.start();
        thread1.start();
    }
}

如果你运行一下的话,停顿会很明显的出现在打印第二个Sleeping...的时候。这是为啥呢?

结合前面讲过的线程运行结果的随机性,我们来分析一下:

main函数内new出来Thread0和Thread1并调用start

这时候调度中心就有三个线程:Thread-main Thread-0 Thread-1

按照下图的运行结果,首先选中Thread-0,执行1行

然后选中Thread-1,执行1行

然后选中Thread-0,执行1行,这是满足了if条件,要执行Thread.sleep()。注意这时候是Thread-0调用run方法,那么当前运行线程就是Thread-0,所以sleep的作用对象就是Thread-0。它的时间片被直接放弃进入阻塞状态。等待1000ms后,阻塞状态结束,Thread-0回到调度中心。

此时调度中心只有Thread-1,立刻选中打印一行代码。此时也满足了if条件,调用Thread.sleep()。那么这个sleep的作用对象就是Thread-1。它的时间片也被放弃进入阻塞。

因为线程的执行是很快很快的,远小于1000ms,那么这是两个线程都在阻塞。调度中心没有线程,那么就等。Thread-0先进入阻塞,那么它先回来。所以就有了后续的交错运行,就不细分析了。

说的这么繁琐是为了让大家理解,sleep的作用对象,这里我写的Thread.sleep。如果我写成这样,作用结果是一样的。因为作用对象是 currently executing thread !!!

当然我这个例子,现实中没人会这么写。只是为了理解这个 currently executing thread。

class MyRunnable implements Runnable{
    public void run(){
        MyRunnable runnable=new MyRunnable();
        Thread thread=new Thread(runnable);
        
        for(int i=1;i<=5;i++){
            System.out.println(Thread.currentThread().getName()+" "+i);
            if(i==2){
                try {
                    System.out.println("Sleeping...");
                    thread.sleep(1000);
                } catch (InterruptedException e) {
                    e.printStackTrace();
                }
            }
        }
    }
}

public class Test {
    public static void main(String[] args){
        MyRunnable runnable=new MyRunnable();
        Thread thread0=new Thread(runnable);

        thread0.start();
    }
}

那么对于这段代码呢,没错睡眠对象就是Thread-main,如果还是不知道当前运行的应该是那个。

可以这么区分

写在main函数里,就是让Thread-main睡

写在哪个线程(的run方法体)里面,就是哪个线程睡

通常我们的写法都是将Thread.sleep() 写在对应线程的代码块中

class MyRunnable implements Runnable{
    public void run(){

        for(int i=1;i<=5;i++){
            System.out.println(Thread.currentThread().getName()+" "+i);
        }
    }
}

public class Test {
    public static void main(String[] args){
        MyRunnable runnable=new MyRunnable();
        Thread thread0=new Thread(runnable);

        thread0.start();
        try {
            thread0.sleep(1000);
        } catch (InterruptedException e) {
            e.printStackTrace();
        }
    }
}

这个当前运行线程十分重要,Thread类中的很多方法都是对当前运行线程起作用。

yield()

yeild()中文的意思叫礼让,这样就很好理解了。他也是个静态方法

作用就是让让前线程暂停执行,进入就绪状态。注意,会使得线程丢弃当前剩余时间片,立马回到调度中心。

API中是这么说的: "A hint to the scheduler that the current thread is willing to yield its current use of a processor. The scheduler is free to ignore this hint."

注意,因为它是回到runnable状态,所以它还可能继续被CPU选中,也有可能选中别的Runnable状态线程。

其实如果细细理解一下的话,它等价于 sleep(0)

这里有个例子,但其实结果并不明显,同样它作为静态方法,作用对象的分析跟前面讲的sleep是一样的就不赘述了。

class MyRunnable implements Runnable{
    public void run(){
        for(int i=1;i<=5;i++){
            System.out.println(Thread.currentThread().getName()+" "+i);
            if(i==2){
                Thread.yield();
            }
        }
    }
}

public class Test {
    public static void main(String[] args){
        MyRunnable runnable=new MyRunnable();
        Thread thread0=new Thread(runnable);
        Thread thread1=new Thread(runnable);
        thread0.start();
        thread1.start();
    }
}

来看看结果,其实并不明显,因为可能还被选中什么的,看这里Thread-0的i是2的时候,调用了yield,直接放弃了Thread-0的时间片(如果还有的话),然后选中了Thread-1,然后巴拉巴拉...然后下面执行到Thread-1 i=2的时候,这时候Thread-0的run已经全部执行完毕了进入死亡状态,所以只能选中Thread-1来继续运行代码。

join()

这个方法不是静态的,但是也比较简单

作用是让产生线程进入阻塞状态,直到相应线程执行完毕。注意,会丢弃剩余的时间片,立刻进入阻塞状态。

也就是说,join方法会阻塞调用此方法的线程进入TIMED_WAITING,直到相应的线程运行完毕才会继续运行。

通常用于main()主线程内,等待其他线程完成再结束主线程。

这个阶段我们就要开始考虑Thread-main的存在了,其实一直都应该考虑,但只是前面的部分主线程的影响并不大,所以暂时忽略了。

来看例子,效果就是我们让thread0,和thread1在Thread-main前执行。也就是Thread-main的结束一定在thread0和thread1都结束之后。而且细细理解一下,thread0和thread1应该是交错的。

class MyRunnable implements Runnable{
    public void run(){
        for(int i=1;i<=5;i++){
            System.out.println(Thread.currentThread().getName()+" "+i);
        }
    }
}

public class Test{
    public static void main(String[] args) {
        MyRunnable runnable=new MyRunnable();
        Thread thread0=new Thread(runnable);
        Thread thread1=new Thread(runnable);

        thread0.start();
        thread1.start();
        try{
            thread0.join();
            thread1.join();
        }catch (Exception e){
            e.printStackTrace();
        }
        System.out.println("Ending--"+Thread.currentThread().getName());
    }
}

结果如图:

你可能会觉得多次一举,那么我们去掉那两行join方法再看看结果,你会发现,Thread-main结束的位置不一定在最后。你可以把Thread-main想成第三个线程。

这三个线程的结果是随机交错的。

线程通讯 -- wait() & notify/notifyAll()

这几个方法的使用较为重要,同时相比之前更加难以理解。

这两个方法是被提取到顶级父类Object()中的,也就是跟我们经常使用的toString()是等同地位的。

我们通过一个故事来简述一下关系及作用。

首先我们要理解,wait 和 notify ,中文意思为 等待 和 通知。

是不是很像人类之间在进行某种活动时的对话,所以这些方法是用于线程之间的通讯。线程就是一个人类活动,当几个人在同时进行这个活动的时候,我们通过 wait 和 notify 方法来进行通讯。

来看一个很形象的例子

打饭的故事

1. 小王 & 小李 (新手程序员)

小王和小李一起来到食堂打饭。在它们打米饭的时候,只有一口锅,只有一个舀米饭的勺子。

小王刚拿起勺子打饭,正在使用的时候,小李就上来抢勺子。

于是两个人抢来抢去,时间都用来抢勺子了,米饭半天都没打满。两个人还打起来了。

这种操作效率低下,也不安全。

程序语言:

小王和小李是两个线程,访问同一个资源的时候(同一锅米饭)

不对其进行加锁控制,就会出现混乱的结果。

也就是我们之前运行结果是交错的,这就是线程不安全。

两个线程可能会同时操作一个共享变量,从而使得这个共享变量失控,最终结果混乱。

2. 王哥 & 李哥 (普通程序员)

王哥和李哥一起来到食堂打饭。在它们打米饭的时候,只有一口锅,只有一个舀米饭的勺子。

王哥先拿起勺子打饭,王哥十分霸道,拿上勺子就谁也不给。

一直等到王哥打完米饭把勺子放回去,李哥才能拿起勺子。

这时候李哥就想,王哥真自私,不懂得礼貌。王哥是个大sb !!!

程序语言:

王哥和李哥是两个线程,访问同一个资源的时候(同一锅米饭)

王哥拿到锁之后,就一直使用,直到王哥使用完毕,也就是synchronized代码块全部执行完毕

这时候李哥才能拿到锁。

这种方式每次都是一个线程执行完,另一个才会执行。这是线程安全的。

3. 老王 & 老李(专家程序员)

老王和老李一起来到食堂打饭。在它们打米饭的时候,只有一口锅,只有一个舀米饭的勺子。

老王先拿起勺子打饭,老王人很好,打了几勺就把勺子让出去了说:“老李你先打,我等会再打”。

老李拿起勺子后,打了几下就通知老王说:老王我打完了,我不用了,你打吧。而且这时候勺子里还有一些米饭。

老王听到后拿起勺子,把勺子里的米饭先放到自己的碗里,然后接着从锅里拿米饭。

两个人你来我往,十分高效,且十分和谐。

程序语言:

老王和老李是两个线程,访问同一个资源的时候(同一锅米饭)

老王先随机拿到了锁,用了一会后,调用wait()方法,将自己的锁交了出去,然后进入等待状态。

老李拿到锁后,使用了一会就调用notify()通知老王,我用完了你来吧。

老王继续拿到锁,继续执行...

这种放保证线程安全的情况下,还合理分配资源

这就是 wait/notify 机制的好处

上面这三个故事很形象的描述了我们的学习路线。

我们最开始接触线程的时候,只知道一昧的多线程,并没有考虑安全。所以结果是随机的,并不是我们想要的。

然后我们学习了锁,在锁的情况下,线程变得安全了。

现在,我们要在线程安全的情况下,引入 wait/notify 机制。更加高效的利用资源,提高代码效率。

wait() notify()/notifyAll() 的使用

下面几个定义结合上面给的打饭的例子来看。

wait()是让当前线程等待,即让线程释放了对共享对象的锁

notify()会让调用wait()系列方法的一个线程释放锁,并通知其他正在等待( 调用了wait() )的线程得到锁。

notifyAll()会唤醒所有在共享变量上由于调用wait系列方法而被挂起的线程。

wait(long timeout)可以指定一个超时时间,过了这个时间如果没有被notify()唤醒,函数还是会返回

调用wait() notify() 时候,当前线程必须要成功获取锁(synchronized),否则抛出异常。总之锁与等待唤醒,是绑在一起的操作。

几个注意点

说实话,这部分比较难,属于进阶编程的部分。如果能在现实项目中游刃有余的应用,那么水平就很高了。

所以对于课内的要求我并不清楚,因为我没细看ppt。

但是它运用到了socket那部分的代码中。如果不理解的话,背就好了应该考不出花样。

对于 等待唤醒 机制的代码,几个点需要明白。

首先,我们说的锁,应该只有一个(对应你要操作的线程来说)。

因为我们 等待唤醒 这个操作的实质就是将这个锁拿来拿去,如果你有多个锁的话,那就没有效果了。所以代码一定是多个线程通过等待唤醒方法将同一个锁拿来拿去。

其次我们之前提到,wait和notify是定义在Object类里面。这是顶级父类,java所有的类全是它的字类。那么每一个对象都可以调用这个方法。

要注意,我们是用锁这个对象来调用 wait和notify的。具体看代码,如果自己写不出来,可以先记住这个结构。以及每一行代码的作用

代码实例

理论看不懂的话,我们直接代码,这是我自己写的一个很直观的例子,只是为了展示这个过程。

代码比较长,而且解释起来比较繁琐。我简要说说:

两个runnable类分别对应两个线程,一个要wait,一个要notify

我们这个流程就是wait线程先进行一部分代码,然后进入wait状态。

这时候notify线程被选中,它调用notify来通知等待线程,你可以继续运行了。

这时候wait线程收到通知,于是跳出等待状态,继续运行后续的代码。

注意我这个锁(obj)的设置方式,以及锁调用wait和notify

class waitRunnable implements Runnable{
    Object obj;
    public waitRunnable(Object obj){
        this.obj=obj;
    }


    public void run(){
        synchronized (obj){

            System.out.println(Thread.currentThread().getName()+ " 获得锁,并且准备等待");
            for(int i=0;i<3;i++){
                System.out.println(i);
            }

            try{
                obj.wait();
            }catch (Exception e){
                System.out.println("等待中");
            }

            System.out.println(Thread.currentThread().getName()+" 被唤醒");
            for(int i=0;i<3;i++){
                System.out.println(i);
            }
        }
    }
}

class notifyRunnable implements Runnable{
    Object obj;
    public notifyRunnable(Object obj){
        this.obj=obj;
    }

    public void run(){
        synchronized(obj){
            System.out.println(Thread.currentThread().getName()+ " 获得锁,并且准备唤醒");

            try{
                System.out.println("发送唤醒");
                obj.notify();
            }catch (Exception e){

            }

        }
    }
}


public class Entrance {


    public static void main(String[] args){
        Object block=new Object();
        waitRunnable wR=new waitRunnable(block);
        notifyRunnable nR=new notifyRunnable(block);


        Thread waitThread=new Thread(wR);
        waitThread.setName("Wait-Thread");

        Thread notifyThread=new Thread(nR);
        notifyThread.setName("Notify-Thread");

        waitThread.start();

        try{
            Thread.sleep(1000);
        }catch (Exception e){
            e.printStackTrace();
        }

        notifyThread.start();
    }
}

运行结果如下,总之有个简单印象,然后ppt给了一个buffer和reader的例子,看懂我写的这个就可以去看那个例子。

 

Interrupt机制

java中有一个线程中断机制

在讲述之前,我们先回顾一些东西,关于java中的异常,包括它的抛出以及处理 。

Java Exception回顾

其实就是关于try catch的理解,经历过小学期,其实大家都已经用的很多很多了。

但是为了后续InterruptionException的理解,这里还是需要再说明一些东西。

当一行代码抛出异常后,Java会做什么?

首先我们知道,如果这行代码有异常抛出,我们必须使用两种方式来进行处理,否则会报错

第一种就是将这个exception继续抛下去不做处理,但是这个exception在抛下去的类中还得处理

第二种就直接在这个类中处理

//method 1
class ClassName throws Exception{

    //code with exception

}


//method 2
class ClassName{

    try{
        
        //code with exception

    }catch(Exception e){
        
        //handle with exception
    }

}

那么对于异常的处理,也就是我写handle with exception的地方。这里发生了什么?

如果我们没有提供异常处理机制,那么程序就会终止,并在Commend line打印

这就是个没有提供异常机制处理的例子,说白了就是代码里写出了bug

public class Test1 {
    public static void main(String[] args) {
        int[] array=new int[5];
        array[6]=0;
    }
}

那么结果就是把这个异常打印出来

 如果我们加上处理机制

public class Test1 {
    public static void main(String[] args) {
        int[] array=new int[5];
        
        try{
            array[6]=0;
        }catch (Exception e){
            //todo
        }
    }
}

那么结果就是这样,因为没有做任何处理,程序只能继续往下走,跑完所有代码退出程序

所以我们对于这个异常,如果加入了异常处理机制,我们就可以在Exception发生时,在todo的位置进行处理。

这个处理是根据你的代码来的

你可以让程序直接中断 :return ;

你可以打印异常的信息:e.printStackTrace() ;

你可以打印一句话:System.out.println("出错了") ;

...

这个程序在发现异常后的走向,是取决于你的代码。明白这一点,我们接着看Interrupt

线程的中断

首先明白,中断不是终止

就是说,就算线程中断了,程序也不一定终止。请先明白这一点

其次

对于每一个线程,都有属于自己的 interrupt flag,默认是false

flag=true的时候,代表有某个线程想让这个线程中断

flag=false的时候,代表一切正常

而且就算有线程想让你这个线程中断,你这个线程也不一定会中断。这是完全无关的

如何理解中断

很绕是吧,你可以这么想

我在外面跟朋友玩,到饭店啦我妈叫我回家。

我正在执行玩这个线程,我妈要执行叫我回家这个线程

叫我回家就是想中断我玩,那么给我玩这个线程的标记设置为了true

代表我妈想让我别玩了,把这个线程中断吧

但是如果我听话的话,我就真的中断了玩,回家了

如果我不听话,我就继续玩,玩这个线程并没有真的中断

而关于我要不要继续玩,也就是这个Thread的后续如何,取决于代码和程序

我们需要手动检查标记或者利用异常处理来实现中断或者忽略中断

所以这也是为什么我在前面回顾 try catch 的原因,这个Thread的后续取决于你的catch框里怎么写,不理解的话继续往后看

InterruptException

还是不理解,我们可以把中断分为三个阶段

1. 设置flag为ture

2. 通过flag抛出InterruptException

3. 处理InterruptException决定线程后续

一点一点来看

怎么设置呢?调用interrupt()方法或者sleep()方法,就是我们之前提到的。这样就将调用线程的标记设置为true。注意,sleep是对当前运行线程起作用,所以设置的也是当前运行线程,就前面讲了一大堆,这里就不演示了。

class MyRunnable implements Runnable{
    public void run(){
        System.out.println("Running");
    }
}

public class Entrance {
    public static void main(String[] args){
        MyRunnable runnable=new MyRunnable();
        Thread thread=new Thread(runnable);
        thread.start();
        try {
            thread.interrupt();
        }catch (Exception e){

        }
        System.out.println(thread.isInterrupted());

    }
}

结果如下

其实我在刚开始学习的时候,以为只要这样调用,就会将线程中断并且抛出中断异常。但其实这一行代码只有设置标记这一个操作。那么中断异常怎样才能抛出呢?

其实下面这段话写的很清楚,大致总结一下

当一个线程处于前面所讲的阻塞状态(调用wait,join,sleep)的时候,你去中断这个线程(给他的flag设置为true),那么这个线程就将抛出InterruptException

关于IO那个ppt好像没讲就不管了

 其实挺好理解的,一个线程调用了sleep睡觉去了,你非要在这个线程睡觉时候中断它

那它肯定不愿意啊,就抛出了中断异常

看一个例子:

class MyRunnable implements Runnable{
    public void run(){
        try{
            Thread.currentThread().sleep(1000);
        }catch (InterruptedException e){
            System.out.println("我在睡觉,别打断我!");
        }
        System.out.println("Sleeping");
    }
}

public class Entrance {
    public static void main(String[] args) {
        MyRunnable runnable=new MyRunnable();
        Thread thread=new Thread(runnable);
        thread.start();
        try{
            System.out.println("我要中断你");
            thread.interrupt();
        }catch (Exception e){

        }
    }
}

运行结果如下,这样就成功抛出了InterruptException

 所欲我们的catch就可以接收到这个Exception,那么我们在catch里面进行处理。

记的我上面说的吗,我们需要手动检查标记或者利用异常处理来实现中断或者忽略中断

这个就属于异常处理,在上面的例子中,我忽略了中断打印了一句话"我在睡觉,别打断我"

如果我们实现中断那么直接这样修改成return,程序就中断了,这个线程也真的中断了

class MyRunnable implements Runnable{
    public void run(){
        try{
            Thread.currentThread().sleep(1000);
        }catch (InterruptedException e){
            return;
        }
        System.out.println("Sleeping");
    }
}

public class Entrance {
    public static void main(String[] args) {
        MyRunnable runnable=new MyRunnable();
        Thread thread=new Thread(runnable);
        thread.start();
        try{
            System.out.println("我要中断你");
            thread.interrupt();
        }catch (Exception e){

        }
    }
}

结果就是这样,当这个线程要中断这个线程的时候,立刻就中断了。同时还有一些细节,当抛出InterruptException后,中断标记会被重置为false。

还有一种方式,是手动检查标记从而实现中断或者忽略中断

Interrupt() isInterrupted()

关于这两个方法都是为了检查中断标记,但是差别很大

isInterrupted()

非静态 作用是检查调用线程的flag标记,返回ture或false

interrupted()

静态方法 作用是检查当前运行的线程的flag标记,返回true或false,并且重置中断标记为false

看到了吗又是当前运行线程,跟前面强调的一样,就不赘述了

(其实上面sleep等方法的重置本质就是这个图中所说的,就是先调用了Interrupted再抛出的异常,所以flag被重置了)

通过这个流程,希望你能明白中断的意义,以及设置flag和真正的中断之间的关系。

要注意什么时候设置了flag,什么时候重置了,什么时候抛出异常了,异常怎么处理的。

用这些问题去具体分析老师的代码。

 

volatile关键字

这个关键字其实讲的地方很多,但是比较超出课程范围,所以有兴趣看看就好了,对线程的理解蛮有帮助的我觉得。可以当休闲看一看。

JVM的内存模型

volatile与JVM的内存模型有关。啥是内存模型呢?

首先我们知道JVM(Java虚拟机)就是为了屏蔽各个硬件平台的差异,大白话就是你的java代码放到随便一个电脑上都可以跑。因为它相当于电脑上安了一个虚拟机,每个电脑上的这个虚拟机都是一样的,所以Java就可以随便到处运行。

那么Java的内存模型就是伴随着JVM,来屏蔽各个硬件平台和操作系统的内存访问差异。以实现让Java程序在各种平台下都能达到一致的内存访问效果。就相当于一块虚拟硬盘,在每个电脑上都一样,这样就屏蔽了电脑的内存差异。

这个内存模型是怎么屏蔽差异的?

Java内存模型规定如下几件事:

所有的变量都存在主存中(物理内存),每个线程都有自己的工作内存(高速缓存)

线程对变量的所有操作必须在工作内存中进行,不能直接对主存进行操作

每个线程不能访问其他线程的工作内存

为啥非得这样,在CPU的运行过程中,会拷贝一份主存的数据到高速缓存中

那么CPU进行计算时候就可以直接向高速缓存读取数据以及写入数据

等到计算结束再将缓存数据刷新到主存。

这样大大提高了运行速度和效率

但是这样会在多线程的时候出问题,也就是我们开头提到的,一个缓存刷新了,其他的缓存并没有跟着刷新,导致最后的结果不正确。

比如线程A给i加了5,线程B给i加了3。但可能最后读入主存的结果只加了5。

这也就是缓存不一致的问题,解决方式就是并发编程

理解并发编程 -- Concurrency

说实话这部分我都是看的一个博主的,我觉得它讲的太好了,我怎么写都是在复制粘贴,我就偷懒直接放链接了。

总之当你认真看完这篇博文,就可以理解为啥我们学的是并发编程,这是怎么实现的。

https://www.cnblogs.com/dolphin0520/p/3920373.htmlicon-default.png?t=LA46https://www.cnblogs.com/dolphin0520/p/3920373.html

老师的ppt就是一坨狗屎,我无力骂他,它的教学顺序是有问题的。

我觉得我这个学习顺序才是线程比较合适的学习顺序。

敲完一遍也基本复习完了,考试顺利🌹

  • 7
    点赞
  • 10
    收藏
    觉得还不错? 一键收藏
  • 5
    评论
评论 5
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

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

抵扣说明:

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

余额充值