Java多线程

  • 一、创建线程四种方式
  • 1)继承Thread
  • 2)调用Runnable
  • 3)匿名内部类
  • 4)使用lambda表达式来创建
  • 二、了解Thread 类
  • 2.1Thread的常见的构造方法
  • 2.2Thread的几个常见的属性
  • 三、启动一个线程
  • 四、中断一个线程
  • 4.1 让线程的入口方法执行完毕
  • 4.2 使用Thread类提供的interrupt方法
  • 五、等待线程
  • 六、线程休眠
  • 七、线程的状态
  • 八、线程安全(重要)!!!
  • 8.1导致线程不安全的原因:
  • 8.2解决线程安全问题方法
  • synchronized
  • volatile
  • 九、对象等待集



一、创建线程四种方式

1)继承Thread

利用多态机制,继承于Thread机制
1)创建一个子类,继承于Thread
2)重写run 方法
3)创建子类实例
4)调用start 方法

public class Demo {
    //创建多线程
    public static void main(String[] args) {
        MyThread2 myThread2=new MyThread2();
        myThread2.start();
    }
}
class MyThread2 extends Thread{
    @Override
    public void run() {
       //这里写线程要执行的代码

    }
}

2)调用Runnable

通过实现Runnable 接口,把Runnable 接口的实例赋值给Thread
1)定义Runnable接口的实现类
2)创建Runnable实现类的实例,并用这个实例作为Thread的target来创建Thread对象,这个Thread对象才是真正的线程对象
3)调用start () 方法

public class Demo {
    //创建多线程
    public static void main(String[] args) {
       //通过Runnable 接口来创建
        Runnable myTask=new MyTask();
        Thread t=new Thread();
        t.start();

    }
}
class MyTask implements Runnable{
    @Override
    public void run() {
        //重写run()方法
    }
}

Runnable 本质上还是要搭配Thread来使用,只不过和直接继承Thread相比,换了一种指定任务的方式而已
这两种方式中Runnable 方式更好一点,能够让线程本身,和线程要执行的任务,更加“解耦合”

3)匿名内部类

通过匿名内部类相当于继承了Thread,作为子类重写run()实现

public class Demo {
    //创建多线程
    public static void main(String[] args) {
       //通过匿名内部类来实现
    Thread t=new Thread(){
        @Override
        public void run() {
        //重写run方法
        }
    };
    t.start();
    }
}

通过Runnable 匿名内部类来实现

public class Demo {
    //创建多线程
    public static void main(String[] args) {
       //通过匿名内部类来实现
   Thread t=new Thread(new Runnable() {
       @Override
       public void run() {
        //重写run方法
       }
    });
   t.start();
    }
}

4)使用lambda表达式来创建

public class Demo {
    //创建多线程
    public static void main(String[] args) {
    Thread t=new Thread(()->{
        //编写线程代码
    }
    );
    t.start();
    }
}

()->{ }这个就是lambda表达式

二、了解Thread 类

2.1Thread的常见的构造方法

Thread()

创建线程对象

Thread(Runnable target)

使用Runnable对象创建线程对象

Thread(String name)

创建线程对象并命名

Thread(Runnable target,String name)

使用Runnable对象来创建线程,并命名

2.2Thread的几个常见的属性

ID

.getId()

名称

.getName()

优先级

.getPriority()

状态

.getState()

是否后台线程

.isDaemon()

是否存活

.isAlive()

是否被中断

.isInterrupted

获取当前线程的实例

currentThread()

优先级和线程调度有关,由操作系统来完成。
后台线程,不影响整个进程的结束
前台线程,会影响到整个进程的结束
是否存活就是run()方法是否运行结束了

三、启动一个线程

start
start 是Thread类的一个关键方法
功能:让操作系统内核真正创建一个线程来执行

start 和run 的区别
start 是创建线程(有新的执行流)
调用run只是一个普通的方法调用,不涉及创建新线程(仍然在原来的线程中,没有涉及到新的执行流)
调用strat 方法

public class Demo {
    //start 和 run 的区别
    public static void main(String[] args) {
        MyThread2 myThread2=new MyThread2();
        myThread2.start();
        while(true){
            System.out.println("hehe");
            try {
                Thread.sleep(1000);
            } catch (InterruptedException e) {
                e.printStackTrace();
            }
        }
    }

}
class MyThread2 extends Thread{
    @Override
    public void run() {
        while(true){
            System.out.println("haha");
            try {
                Thread.sleep(1000);
            } catch (InterruptedException e) {
                e.printStackTrace();
            }
        }
    }
}

java中怎么保证多线程的运行安全 java多线程安全的实现方式_自增


可以看到两个线程并发执行,

而如果是调用run()方法,就是普通的调用,没有创建新线程,一直在循环里出不来

java中怎么保证多线程的运行安全 java多线程安全的实现方式_线程安全_02

四、中断一个线程

4.1 让线程的入口方法执行完毕

线程执行完毕,运行5S 线程执行完毕

public class Demo {
    //中断一个线程
    static boolean isRunning=true;
    public static void main(String[] args) {
        Thread t1=new Thread(){
            @Override
            public void run() {
                while(isRunning){
                    System.out.println("hello");
                    try {
                        Thread.sleep(1000);
                    } catch (InterruptedException e) {
                        e.printStackTrace();
                    }
                }
            }
        };
        t1.start();

        try {
            Thread.sleep(5000);
        } catch (InterruptedException e) {
            e.printStackTrace();
        }
        isRunning=false;
        System.out.println("线程运行5S结束");
    }
}

4.2 使用Thread类提供的interrupt方法

针对上面的方式进行修改
1,把上面的while()中判断条件进行修改
2,把catch里面的代码,加一个break
调用 interrupt()是通知线程结束,具体还是看内部代码的实现

public class Demo {
   //使用Thread 方法来中断线程
   public static void main(String[] args) {
       Thread t1=new Thread(){
           @Override
           public void run() {
               while(!Thread.currentThread().isInterrupted()){
                   System.out.println("hello");
                   try {
                       Thread.sleep(1000);
                   } catch (InterruptedException e) {
                      // e.printStackTrace();
                       break;
                   }
               }
           }
       };
       t1.start();

       try {
           Thread.sleep(5000);
       } catch (InterruptedException e) {
           e.printStackTrace();
       }
       System.out.println("线程结束");
       t1.interrupt();
   }
}

五、等待线程

我们在创建多个线程之后,每个线程都是一个独立的执行流~
这些线程每个线程的执行顺序都是不确定的,完全取决于操作系统的调度,这里的等待线程机制就是一种确定线程先后顺序方式,确定线程的结束顺序,无法确定谁先开始,可以确定谁先结束 使用join()
join 起到的效果就是等待某个线程结束,谁调用join就等待谁结束
通过代码来解释:

public class Demo {
   //使用Thread 方法来中断线程
   public static void main(String[] args) {
       Thread t1=new Thread(){
           @Override
           public void run() {
            for(int i=0;i<5;i++){
                System.out.println("hello");
                try {
                    Thread.sleep(1000);
                } catch (InterruptedException e) {
                    e.printStackTrace();
                }
            }

           }
       };
       t1.start();
       try {
           t1.join();
       } catch (InterruptedException e) {
           e.printStackTrace();
       }

   }
}

这里在main 方法中调用了join,相当于在主线程中,等待t1线程结束
main方法在执行的时候,遇到join就会堵塞等待,一直等t1线程执行完毕,这个时候join才会继续往下执行
也就是说谁调用join 谁先结束

六、线程休眠

当执行sleep时就是让线程休眠,所谓的休眠就是把线程的task struct放入等待队列
CPU在执行的时候是挑等待队列中的线程来执行的,而sleep的线程在等待队列不在就绪队列,所以不会被执行
也可以在sleep中加上时间,等时间过去后,等待队列的线程才有机会到就绪队列,至于什么时候到就绪队列还是要看调度器执行

等待队列可能有好多了,具体谁先出队列,先回到就绪队列和设定的时间相关,如果时间一样就看系统的调度

七、线程的状态

在我们调试多线程程序有帮助

NEW

Thread 对象刚创建,还没有在系统中创建线程,相当于任务交给了线程,但是线程还没有开始执行

Runnable

线程是一个准备就绪的状态,随时可能调度到CPU上执行,或者正在CPU上执行(线程的task struct在就绪队列中)

Blocked

线程堵塞(线程在等待队列里)没有竞争到锁

Waiting

线程堵塞(线程在等待队列里)调用waiting 方法

Timed_Waiting

线程堵塞(线程在等待队列里)调用sleep方法

Terminated

线程结束了(Thread对象还没销毁)

八、线程安全(重要)!!!

多线程虽然是更轻量的并发编程(相比于进程),但是线程是访问同一份内存资源,由于线程是一个抢占式执行的过程中谁先执行,谁后执行,不确定,完全却决于系统的调度。由于这里不确定性太多就可能导致多个线程访问同一个资源的时候,出现BUG所以引出了线程安全问题。 访问分为读和写操作,读操作不会涉及到线程安全问题,只有写操作涉及线程安全工作

多线程修改同一变量

public class Demo {
   //线程安全问题
    static class Counter{
        //创建一个自增类,通过线程调度来展示线程安全问题
        public int count=0;

        public void increase(){
            count++;
        }
   }

    public static void main(String[] args) {
        //通过两个线程同时对count进行自增
        Counter counter=new Counter();
        Thread t1=new Thread(){
            @Override
            public void run() {
                for(int i=0;i<50000;i++){
                    counter.increase();
                }
            }
        };
        Thread t2=new Thread(){
            @Override
            public void run() {
                for(int i=0;i<50000;i++){
                    counter.increase();
                }
            }
        };

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

        try {
            t1.join();
            t2.join();
        } catch (InterruptedException e) {
            e.printStackTrace();
        }
        System.out.println(counter.count);
    }
}

此处代码就是t1,t2 两个线程修改同一变量,存在线程安全问题,正常情况下count值应该是100000,但是运行下来,count值是在50000~100000之间,每次都在变化,这是为什么呢? 这里就是触发了线程安全的问题

这里我们先看count ++ 具体做了什么事情,这里我们就要引入JMM了

JMM(JVM 实现方式的抽象,Java 程序和内存之间是如何交互的)

1)先把内存数据读取到CPU的寄存器中

2)针对寄存器中的内容,通过类似于ADD这样的指令进行+1,操作的结果仍然是放在寄存器中里

3)把寄存器中的数据,写回到内存中

java中怎么保证多线程的运行安全 java多线程安全的实现方式_自增_03


由于多线程之间是抢占式执行的,可能第一个线程执行自增一半时,就可能被调度出CPU,由第二个线程再次自增

java中怎么保证多线程的运行安全 java多线程安全的实现方式_java中怎么保证多线程的运行安全_04


LOAD就是从内存中读取数据到寄存器 ADD就是进行自增效果 SAVE就是将寄存器中数据写回内存中

如果出现第一个线程读取到数据为0时,在进行自增操作时,线程二也进行读取数据,两个线程都读到的是0,相当于两次自增操作只增加了一次,只要是线程二的读取不是在线程一SAVE操作后,就会发生自增异常的情况!所以两个线程分别自增50000次,数据最后的数值是在50000~100000之间的。这就是抢占式执行同一个资源所带来的异常!

如果这里是两个CPU也是同样的情况,这样的不确定性,不符合预期的要求,就认为是BUG,因为我们执行代码就是追求的是确定性

8.1导致线程不安全的原因:

由于多线程是抢占式执行,可能会出现第一个线程自增执行到一半就被调度出去,就会执行第二个线程。

1)线程的抢占式执行过程(无法修改)操作系统内核实现的
2)多个线程修改同一变量
3)修改操作不是原子性 (原子性:不可拆分)如果三个操作(读取,自增,写回)打包成一个整体,这就能解决了线程不安全问题,保证操作原子性,是保证线程安全的主要手段
4)内存可见性
两个线程同时操作一个内存,比如一个读一个写,写操作的线程进行修改时,读线程读取到的可能是修改之前的结果,也可能是读取到修改之后的结果,也会带来线程安全问题
内存可见性也可能是编译器优化,假设执行一个循环自增,这样的操作就涉及到大量的读写操作,读写内存的操作比访问CPU寄存器要慢几千倍,所以JVM往往对指令进行优化,把它等价转换成另外一种情况 保证逻辑不变的情况下,读取一次内存,之后进行自增,自增结束后在写回内存,节省了很多的 读写内存的开销,但是这样会触发线程不安全。

解决可见性,方案就是直接禁止这样的编译器的优化,让程序跑慢点,关键是要对,不能在多线程情况下出错

5)指令重排序
和线程不安全直接相关,也是和编译器优化直接相关,为了让程序跑的更快,调整了执行顺序~(调整的前提逻辑不改变,但是效率提高),如果是多线程的情况下可能重排会改变逻辑,会导致线程不安全问题。

8.2解决线程安全问题方法

1)多线程不修改同一变量

synchronized

synchronized(关键字) 监视器锁 ,我们通过加锁来保证操作原子性,同时禁止指令重排序和保证内存可见性

用法:

1.修饰一个方法 (方法前加上,就是针对代码进行加锁工作,调用方法就加锁,出了代码块就解锁)

java中怎么保证多线程的运行安全 java多线程安全的实现方式_加锁_05

2.修饰一个代码块(包裹起来) 针对哪个对象加锁,括号内就填哪个对象

java中怎么保证多线程的运行安全 java多线程安全的实现方式_加锁_06

分析synchroized 工作过程
使用synchronized 就是 相当于增加于给操作增加了两个指令 LOCK UNLOCK
LOCK 操作的特性,只有一个线程能执行成功,直到另一个线程释放UNLOCK 另一个线程才能执行
就比如上面演示的自增操作,加锁就相当于把LOAD ADD SAVE 三个操作打包为一个操作,这样就解决了线程的原子性,并且synchronized也能禁止编译器的进行内存可见性和指令重排序,所以使用synchronized就解决了线程安全问题,但是synchronized也付出了代价,程序运行的效率大大降低了。

注意synchroized 括号内填什么
针对哪个对象加锁,就填哪个对象,每一个对象能都加锁,如果多个线程竞争同一锁对象(尝试对同一个对象加锁,此时就会出现一个竞争成功,其他等待的情况),如果两个线程竞争不同的锁,两个线程都能成功获取到锁。

如果synchronized修饰的就是方法,相当于加锁的对象是this
如果synchronized修饰静态方法,相当于加锁的对象是类对象

错误示范:1)加锁对象错误

java中怎么保证多线程的运行安全 java多线程安全的实现方式_java中怎么保证多线程的运行安全_07


此时由于我们加锁的不是同一个对象,第一个为t1,第二个是t2,这两个加锁操作就不会构成竞争,没有作用2)嵌套加锁 :

java中怎么保证多线程的运行安全 java多线程安全的实现方式_java中怎么保证多线程的运行安全_08


这里我们在给increase方法加了锁,还对counter加了一次锁,相当于连续加了两次锁,这会产生什么情况?

会产生死锁,因为

1)执行程序时,运行到循环时,因为对counter 对象就行了加锁,此时用LOCK将counter 对象锁起来

2)当程序运行到调用increase () 方法时,这个方法也有加锁,但是此时无法加锁,因为有锁在上面,所以increase ()方法就进入堵塞等待,等待上一个加锁操作进行释放~

3)但是上一个加锁释放是要执行完increase ()方法的,但是此时方法无法执行,这里就产生了死锁情况!

但是如果运行程序的话,可以运行成功,因为synchroized 内部对这种状况进行了解决,利用特殊的手段来处理这个场景“可重入锁
synchroized 如何实现的可重入锁效果?
synchroized内部记录了当前这把锁时哪个线程持有的, 如果当前加锁线程和持有线程是同一线程,而不是真的进行加锁,而是把一个计数器++ ,如果后续该线程继续尝试获取锁,继续判定加锁线程和持有线程是不是同一线程,只要是同一线程,就不真正加锁,而是计数器++,如果该线程调用解锁操作,也不是立即解锁,而是计数器- - ,直到计数器减为0了,才认为真的要“释放锁了 ”,才允许其他线程来获取锁~

volatile

起到的效果也是辅助保证线程安全~~ 主要是用于读写同一个变量的时候

volatile能够禁止指令重排序,和内存可见性,但是不能保证原子性

我们来设计一个场景 线程一进行循环,线程二通过修改循环条件,来使得线程1循环结束

import java.util.Scanner;

public class Demo {
  static class Counter{
      public int flag=0;
  }

    public static void main(String[] args) {
        Counter counter=new Counter();
            Thread t1=new Thread(){
                @Override
                public void run() {
                    while(counter.flag==0){
                        //do nothing
                    }
                    System.out.println("线程一循环结束");
                }
            };
            t1.start();
        Thread t2=new Thread(){
            @Override
            public void run() {
                Scanner scanner=new Scanner(System.in);
                System.out.println("请输入一个整数");
                counter.flag=scanner.nextInt();
            }
        };
        t2.start();
    }
}

我们预想的效果是输入一个非0整数之后,循环停止,但是运行程序发现,循环并没有停止,这是因为内存可见性,如果没有优化,此时CPU就需要频繁的读取内存数据,但是这时进行了优化,编译器就把这个读操作优化成只从内存中读一次,后续都直接读寄存器中数值,即使内存中数值发生变化,也不会读取到,
这种场景情况下,使用synchroized也可以,但是没必要因为volatile 比synchroized更轻量化,解决方法就是在flag 前加上volatile,加上volatile之后线程一每次读取flag的值都必须从内存中读取了(效率降低了,但是代码逻辑准确了)
但是volatile只使用于一读一写的情况,如果多个线程都要执行写操作,那么volatile就没有作用了,就要使用synchroized了

九、对象等待集

功能: 协调多个线程之间执行的先后顺序

对象等待集的应用场景

由于多线程之间是一个”抢占式执行“,可能会导致某个线程一直占用,其他线程就会出现线程饿死的情况,等待集就是解决线程太频繁占用,

实现等待集: wait () ,notify() ,notifyAll ()
wait /notify 这一系列方法必须搭配,synchronized 来使用,如果不在synchronized 使用就会出现异常,因为当前预期是获取到锁的状态才能调用wait(),没有synchronized相当于还没获取到锁,就尝试调用,于是就会出现异常

wait 内部做了三件事
1.释放锁
2. 等待其他线程的通知
3.等待通知之后,重新尝试获取锁
notify()

通知某个线程被唤醒,从wait中醒来,notify也是在synchroized中使用,调用notify()方法之后,代码不会立即释放锁,而是在执行完当前的synchroized之后才释放锁,同时等待中的线程就尝试重新竞争这个锁

演示wait 和 notify 用法

//演示wait 和 notify 用法
public class Demo {
    //创建一个锁对象
    static public Object Locker=new Object();
   //用来等待的线程
    static class WaitTask implements Runnable{
       @Override
       public void run() {
        synchronized (Locker){
            while (true){
                try {
                    System.out.println("wait 开始");
                    Locker.wait();
                    System.out.println("wait 结束");
                } catch (InterruptedException e) {
                    e.printStackTrace();
                }
            }
        }
       }
   }
    //用来通知的线程
    static class NoitfyTask implements Runnable{
        @Override
        public void run() {
        synchronized (Locker){
            System.out.println("nofity 开始");
            Locker.notify();
            System.out.println("nofity 结束");
        }
        }
    }

    public static void main(String[] args) throws InterruptedException {
        Thread t1=new Thread(new WaitTask());
        Thread t2=new Thread(new NoitfyTask());
        t1.start();
        Thread.sleep(3000);
        t2.start();

    }
}

java中怎么保证多线程的运行安全 java多线程安全的实现方式_java中怎么保证多线程的运行安全_09


1.先运行WaitTask(),执行到wait ()方法后,释放锁并等待通知

2.3S后开始执行nofity Task 方法

3.执行nofity Task()方法之后,唤醒WaitTask 线程,WaitTask就从WAITING 状态醒来,尝试竞争锁,由于当前锁没有被nofity Task释放,于是竞争锁失败,进入堵塞队列,进入BLOCKED 状态

4.NoitfyTask执行完毕后,锁就被释放了,WaitTask()才能够竞争到锁,于是就从wait内部返回了,于是继续执行,打印“wait 结束”

5.继续进入下次循环在进行等待。