Java多线程?一篇就够了

Posted Serendipity sn

tags:

篇首语:本文由小常识网(cha138.com)小编为大家整理,主要介绍了Java多线程?一篇就够了相关的知识,希望对你有一定的参考价值。

多线程详解

一.认识线程及线程的创建

1.线程的概念

线程和进程的区别:
进程是系统分配资源的最小单位,线程是系统调度的最小单位。
一个进程内的线程之间是可以共享资源的。
每个进程至少有一个线程存在,即主线程。

注:
每个进程至少有一个线程存在,即主线程(系统级别的,C语言的主线程)
java级别的主线程(自己写的入口函数main方法(可以没有这个线程)
对java进程来说,至少有一个非守护线程还没终止,进程就不会结束

2.线程的特性

在后面线程的安全性会详细介绍
1.原子性:即一个操作或者多个操作 要么全部执行并且执行的过程不会被任何因素打断,要么就都不执行。
2.可见性:当多个线程访问同一个变量时,一个线程修改了这个变量的值,其他线程能够立即看得到修改的值。
3.有序性:程序执行的顺序按照代码的先后顺序执行。

3.线程的创建方式

<1>继承Thread类

class MyThread extends Thread{
    @Override
    public void run() {
        System.out.println("继承Thread类创建线程");
    }
}
 public static void main(String[] args) {
        //1.继承Thread类创建线程
        MyThread t=new MyThread();
        t.start();
        }

<2>实现Runnable接口

  1. 将MyRunnable对象作为任务传入Thread中
class MyRunnable implements Runnable{
    @Override
    public void run() {
        System.out.println("继承Runnable接口,创建描述任务对象,实现多线程");
    }
}
  public static void main(String[] args) {
     
        //2.实现Runnable接口
        Thread t1=new Thread(new MyRunnable());
        t1.start();
        }

2.使用匿名内部类实现

 Thread t2=new Thread(new Runnable() {
            @Override
            public void run() {
                System.out.println("使用Runnable接口,创建匿名内部类实现");
            }
        });
        t2.start();

<3>实现Callable接口

实现Callable重现call方法,允许抛出异常,允许带有返回值,返回数据类型为接口上的泛型

class MyCallable implements Callable<String> {
    //允许抛出异常,允许带有返回值,返回数据类型为接口上的泛型
    @Override
    public String call() throws Exception {
        System.out.println("实现了Callable接口");
        return "这不是一个线程类,而是一个任务类";
    }
}
public static void main(String[] args) throws ExecutionException, InterruptedException {
        //方法三:实现Callable接口,是一个任务类
        //FutureTask底层也实现了Runnable接口
        FutureTask<String> task=new FutureTask<>(new MyCallable());
        new Thread(task).start();
        System.out.println(task.get());
    }

二.线程的常用方法

1.构造方法和属性的获取方法

构造方法:
在这里插入图片描述
属性的获取方法:
在这里插入图片描述

2.常用方法

<1>run()和start()

start();方法:启动线程
run();方法:覆写 run 方法是提供给线程要做的事情的指令清单

start()和run()的区别:见代码

public class Thread_Run_VS_Start {
    public static void main(String[] args) {
        new Thread(new Runnable() {
            @Override
            public void run() {
                while (true){

                }
            }
        }).run();
        /**
         * main线程直接调用Thread对象的run方法会直接在main线程
         * 运行Thread对象的run()方法---->传入的runnable对象.run()
         * 结果,main线程直接运行while(true)
         *
         * start()是启动一个线程,调用新线程的while(true)方法
         * 对比通过start()调用的结果区别
         */

        new Thread(new Runnable() {
            @Override
            public void run() {
                while (true){

                }
            }
        }).start();
    }
}

<2>interrupt()方法

在这里插入图片描述

通过interrupt()方法,通知线程中的中断标志位,由false变为true,但是线程什么时候中断,需要线程自己的代码实现
通过线程中的中断标志位实现,比起自己手动设置中断标志位,可以避免线程处于阻塞状态下,无法中断的情况

对interrupt,isInterrupt,interrupted的理解:
实例方法:
(1)interrupt:置线程的中断状态
如果调用该方法的线程处于阻塞状态(休眠等),会抛出InterruptedException异常
并且会重置Thread.interrupted;返回当前标志位,并重置
(2)isInterrupt:线程是否中断,返回boolean
静态方法:
(3)interrupted:返回线程的上次的中断状态,并清除中断状态

public class Interrupt {
    public static void main(String[] args) throws InterruptedException {
        Thread t=new Thread(new Runnable() {
            @Override
            public void run() {

                //...执行任务,执行时间可能比较长
               //运行到这里,在t的构造方法中不能引用t使用Thread.currentThread()方法,获取当前代码行所在线程的引用
                for (int i = 0; i <10000&&!Thread.currentThread().isInterrupted() ; i++) {
                    System.out.println(i);
                    //模拟中断线程
                    try {
                        Thread.sleep(1000);
                        //通过标志位自行实现,无法解决线程阻塞导致无法中断
                        //Thread,sleep(100000)
                    } catch (InterruptedException e) {
                        e.printStackTrace();
                    }
                }
            }
        });
        t.start();//线程启动,中断标志位=false
        System.out.println("t start");
        //模拟,t执行了5秒,进程没有结束,要中断,停止t线程
        Thread.sleep(5000);

        //未设置时,isInterrupt为false

        //如果t线程处于阻塞状态(休眠等),会抛出InterruptedException异常
        //并且会重置isInterrupt中断标志位位false
        t.interrupt();//告诉t线程,要中断(设置t线程的中断标志位为true),由t的代码自行决定是否要中断
        //isInterrupt设置为true
        //t.isInterrupted();  Interrupted是线程中的标志位
        System.out.println("t stop");


        //注:Thread.interrupted(); 返回当前线程的中断标志位,然后重置中断标志位
         
    }
}

<3>join方法

注意: join方法是实例方法
等待一个线程执行完毕,才执行下一个线程(调用该方法的线程等待)
在这里插入图片描述
无参:t.join:当前线程无条件等待,直到t线程运行完毕
在这里插入图片描述有参:t.join(1000)等待1秒,或者t线程结束,哪个条件满足,当前线程继续往下执行

//join方法:实例方法:
// 1.无参:t.join:当前线程无条件等待,直到t线程运行完毕
//  2.有参:t.join(1000)等待1秒,或者t线程结束,哪个条件满足,当前线程继续往下执行
public class Join {
    public static void main(String[] args) throws InterruptedException {
        Thread t=new Thread(new Runnable() {
            @Override
            public void run() {
                System.out.println("1");
            }
        });
        t.start();

        t.join();//当前线程main线程无条件等待,直到t线程执行完毕,当前线程再往后执行
       // t.join(1000);当前线程等到1秒,或者等t线程执行完毕
        System.out.println("ok");

    }
}

<4>获取当前线程的引用currentThread();方法

静态方法:
在这里插入图片描述

public class ThreadDemo { 
public static void main(String[] args) { 
Thread thread = Thread.currentThread(); 
System.out.println(thread.getName()); 
} 
}

<5>休眠当前线程sleep();方法

让线程等待一定时间后,继续运行
在这里插入图片描述

Thread.sleep(1000);

<6>线程让步yield();方法

让yield();所在代码行的线程让步,当其他线程先执行

public class Yield {
    public static void main(String[] args) {
        for(int i=0;i<20;i++){
            final int n=i;
            Thread t=new Thread(new Runnable() {
                @Override
                public void run() {
                    System.out.println(n);
                }
            });
            t.start();
        }
        //判断:如果活跃的线程数量大于1,main线程让步
        while (Thread.activeCount()>1){//记录活跃线程的数量
            Thread.yield();
        }//注意:要用debug方式,因为run方式,idea后台还会启动一个线程
        //实现ok在1到二十之后打印
        System.out.println("ok");
    }
}

三.线程的生命周期和状态转换

Java 语言中线程共有六种状态,分别是:

NEW(初始化状态)

RUNNABLE(可运行 / 运行状态)

BLOCKED(阻塞状态)

WAITING(无时限等待)

TIMED_WAITING(有时限等待)

TERMINATED(终止状态)

生命周期和状态转换图:
在这里插入图片描述
常见的API导致的状态转换:
1.线程的阻塞:
Thread.sleep(long);当前线程休眠
t.join/t.join(long);t线程加入当前线程,当前线程等待阻塞
synchronized:竞争对象锁失败的线程,进入阻塞态
2.线程的启动:
start() ----->注意:run()只是任务的定义,start()才是启动
3. 线程的中断:interrupt让某个线程中断,不是直接停止线程,而是一个“建议”,是否中断,由线程代码自己决定

四.线程间的通信

wait(0方法:线程等待
notify();方法:随机唤醒一个线程
notifyAll():方法:唤醒所有等待的线程
注意:这三个方法都需要被Synchronized包裹x
在这里插入图片描述
线程间通信的案例:
有三个线程,每个线程只能打印A,B或C
要求:同时执行三个线程,按ABC顺序打印,依次打印十次
ABC换行 ABC换行。。。。

public class SequencePrintHomeWork {
    //有三个线程,每个线程只能打印A,B或C
    //要求:同时执行三个线程,按ABC顺序打印,依次打印十次
    //ABC换行 ABC换行。。。。
    //考察知识点:代码设计,多线程通信

    public static void main(String[] args) {
        Thread a = new Thread(new Task("A"));
        Thread b = new Thread(new Task("B"));
        Thread c = new Thread(new Task("C"));
        c.start();
        b.start();
        a.start();
    }

    private static class Task implements Runnable{

        private String content;
        //顺序打印的内容:可以循环打印
        private static String[] ARR = {"A", "B", "C"};
        private static int INDEX;//从数组哪个索引打印

        public Task(String content) {
            this.content = content;
        }

        @Override
        public void run() {
            try {
                for(int i=0; i<10; i++){
                    synchronized (ARR){//三个线程使用同一把锁
                        //从数组索引位置打印,如果当前线程要打印的内容不一致,释放对象锁等待
                        while(!content.equals(ARR[INDEX])){
                            ARR.wait();
                        }
                        //如果数组要打印的内容和当前线程要打印的一致,
                        // 就打印,并把数组索引切换到一个位置,通知其他线程
                        System.out.print(content);
                        if(INDEX==ARR.length-1){
                            System.out.println();
                        }
                        INDEX = (INDEX+1)%ARR.length;
                        ARR.notifyAll();
                    }
                }
            } catch (InterruptedException e) {
                e.printStackTrace();
            }
        }
    }
}

补充: wait()和sleep()的区别:

wait 之前需要请求锁,而wait执行时会先释放锁,等被唤醒时再重新请求锁。这个锁是 wait 对象上的 monitor
lock
sleep 是无视锁的存在的,即之前请求的锁不会释放,没有锁也不会请求。
wait 是 Object 的方法
sleep 是 Thread 的静态方法

五.多线程的安全及解决

1.原子性

对原子性的理解: 我们把一段代码想象成一个房间,每个线程就是要进入这个房间的人。如果没有任何机制保证,A进入房间之后,还没有出来;B 是不是也可以进入房间,打断 A 在房间里的隐私。这个就是不具备原子性的。
注意: 一条 java 语句不一定是原子的,也不一定只是一条指令
例如:
在这里插入图片描述
如果一个线程正在对一个变量操作,中途其他线程插入进来了,如果这个操作被打断了,结果就可能是错误的。

2.可见性

为了提高效率,JVM在执行过程中,会尽可能的将数据在工作内存中执行,但这样会造成一个问题,共享变量在多线程之间不能及时看到改变,这个就是可见性问题。
在这里插入图片描述

可见性:
系统调度CPU执行线程内,某个方法,产生CPU视角的主存,工作内存
主存:线程共享
工作内存:线程私有内存+CPU高速缓存/寄存器
对主存中共享数据的操作,存在主存到工作内存<====>从主存读取,工作内存修改,写回主存(拷贝)

3.代码的顺序性

代码的重排序:
一段代码:
1.去前台取下 U 盘
2. 去教室写 10 分钟作业
3. 去前台取下快递
如果是在单线程情况下,JVM、CPU指令集会对其进行优化,比如,按 1->3->2的方式执行,也是没问题,可以少跑一次前台。这种叫做指令重排序

代码重排序会给多线程带来什么问题:
刚才那个例子中,单线程情况是没问题的,优化是正确的,但在多线程场景下就有问题了,什么问题呢。可能快递是在你写作业的10分钟内被另一个线程放过来的,或者被人变过了,如果指令重排序了,代码就会是错误的。
在这里插入图片描述

4.线程不安全问题的解决

<1>synchronized 关键字

这里会在下面锁体系中详细说

<2>volatile 关键字

volatile 关键字的作用:
(1)保证可见性
(2)禁止指令重排序,建立内存屏障——单例模式说明
(3)不保证原子性
常见的使用场景:一般是读写分离的操作,提高性能
(1)写操作不依赖共享变量,赋值是一个常量(依赖共享变量的赋值不是原子性操作)
(2)作用在读,写依赖其他手段(加锁)

一个volatile的简单例子:

public class Test {
    private static boolean flag = true;
    public static void main(String[] args) {
        //创建一个线程并启动
        new Thread(new Runnable() {
            int i=0;
            @Override
            public void run() {
                while(flag){
                    //这个语句底层使用了synchronized,保证了可见性
                    //System.out.println("=============");

                    i++;
                }
            }
        }).start();
        try {
            Thread.sleep(1000);
        } catch (InterruptedException e) {
            e.printStackTrace();
        }
        //即使改了,上面的线程flag也不会改,会一直循环
        flag = false;
    }
}

六.锁体系

多线程中锁的作用:保证线程的同步

1.Synchronized加锁方式

<1>Synchronized的加锁方式及语法基础

如何解决上述原子性例子的问题:
是不是只要给房间加一把锁,A 进去就把门锁上,其他人是不是就进不来了。这样就保证了这段代码的原子性了。有时也把这个现象叫做同步互斥,表示操作是互相排斥的。
synchronized 关键字:
(1)作用:对一段代码进行加锁操作,让某一段代码满足三个特性:原子性,可见性,有序性
(2)原理:多个线程间同步互斥(一段代码在任意一个时间点,只有一个线程执行:加锁,释放锁)
注意: 加锁/释放锁是基于对象来进行加锁和释放锁,不是把代码锁了

只有对同一个对象加锁,才会让线程产生同步互斥的效果:
那么怎样才叫对同一个对象加锁呢?
这里t代表类名,t1,t2是 new了两个t increment是t中的一个方法(是静态还是实例具体看)
在这里插入图片描述
synchronized处加锁,抛出异常或代码块结束释放锁
在这里插入图片描述
具体过程:
在这里插入图片描述
synchronized 多个线程n同步互斥:
(1):一个时间只有一个线程执行(同步互斥)
(2):竞争失败的线程,不停的在阻塞态和运行态切换(用户态和内核态切换)
(3)同步线程数量越多,性能越低

一个简单的小例子:

public class SafeThread {
    //有一个遍历COUNT=0;同时启动20个线程,每个线程循环1000次,每次循环把COUNT++
    //等待二十个子线程执行完毕之后,再main中打印COUNT的值
    //(预期)count=20000
    private static int COUNT=0;

    //对当前类对象进行加锁,线程间同步互斥
//    public synchronized static void increment(){
//        COUNT++;
//    }


    //使用不同的对象加锁,没有同步互斥的效果,并发并行
//    public static void increment(){
//        synchronized (new SafeThread()){
//            COUNT++;
//        }
//    }
    public static void main(String[] args) throws InterruptedException {
        //尽量同时启动,不让new线程操作影响
        Class clazz=SafeThread.class;
      Thread[]threads=new Thread[20];
        for Java 中的多线程你只要看这一篇就够了

Java中的多线程你只要看这一篇就够了

Java中的多线程你只要看这一篇就够了

Java中的多线程你只要看这一篇就够了

Java中的多线程你只要看这一篇就够了

Java中的多线程你只要看这一篇就够了