纯净、安全、绿色的下载网站

首页|软件分类|下载排行|最新软件|IT学院

当前位置:首页IT学院IT技术

java多线程从入门到精通 java多线程从入门到精通看这篇就够了

Serendipity sn   2021-06-07 我要评论
想了解java多线程从入门到精通看这篇就够了的相关内容吗Serendipity sn在本文为您仔细讲解java多线程从入门到精通的相关知识和一些Code实例欢迎阅读和指正我们先划重点:java多线程,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,但是线程什么时候中断,需要线程自己的代码实现

通过线程中的中断标志位实现,比起自己手动设置中断标志位,可以避免线程处于阻塞状态下,无法中断的情况

对interruptisInterruptinterrupted的理解

实例方法

(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();
        }
        //判断:如果活跃的线程数量大于1main线程让步
        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

在这里插入图片描述

线程间通信的案例:

有三个线程每个线程只能打印AB或C

要求:同时执行三个线程按ABC顺序打印依次打印十次

ABC换行 ABC换行

public class SequencePrintHomeWork {
    //有三个线程每个线程只能打印AB或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代表类名t1t2是 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 (int i = 0; i <20 ; i++) {
            threads[i]=new Thread(new Runnable() {
                @Override
                public void run() {
                    for (int j = 0; j <1000 ; j++) {
                        //给SafeThread对象加一把锁
                        synchronized (clazz){
                            COUNT++;
                        }
                    }
                }
            });
        }
        for (int i = 0; i <20 ; i++) {
            threads[i].start();
        }
        //让main线程等待20个子线程运行完毕
        for (int i = 0; i <20 ; i++) {
            threads[i].join();
        }
        System.out.println(COUNT);
    }
}

synchronized加锁的缺点

a)如果获取锁的线程由于要等待IO或其他原因(如调用sleep方法)被阻塞了但又没有释放锁其他线程只能干巴巴地等待此时会影响程序执行效率

b)只要获取了synchronized锁不管是读操作还是写操作都要上锁都会独占如果希望多个读操作可以同时运行但是一个写操作运行无法实现

<2>Synchronized的原理及实现

1.Monitor机制

(1)基于monitor对象的监视器:使用对象头的锁状态来加锁

(2)编译为字节码指令为:1个monitoren+2个monitorexit 多出来的一个monitorexit:如果出现异常第一个monitorexit无法正确释放锁这个monitorexit进行锁释放

例如下列代码

public class Test1 {
    public Test1() {
    }
    public static void main(String[] args) {
        Class var1 = Test1.class;
        synchronized(Test1.class) {
            System.out.println("hello");
        }
    }
}

反编译

在这里插入图片描述

(3)monitor存在计数器实现synchronized的可重入性:进入+1退出-1

<3>JVM对Synchronized的优化

(1).对锁的优化

Synchronized是基于对象头的锁状态来实现的从低到高:(锁只能升级不能降级)

(1)无锁

(2)偏向锁:对同一个对象多次加锁(重入)

(3)轻量级锁:基于CAS实现同一个时间点经常只有一个线程竞争锁

(4)重量级锁:基于系统的mutex锁同一个时间点经常有多个线程竞争

特点:mutex是系统级别的加锁线程会由用户态切换到内核态切换的成本比较高(一个线程总是竞争失败就会不停的在用户态和内核态之间切换比较耗费资源进一步如果很多个竞争失败的线程性能就会有很大的影响)

(2).锁粗话

多个synchronized连续执行加锁释放锁可以合并为一个

示例:StringBuffer静态变量在一个线程中多次append(静态变量属于方法区jdk 1.8后是在堆里面线程共享)

public class Test {
    private static StringBuffer sb;
    public static void main(String[] args) {
        sb.append("1").append("2").append("3");
    }
}
(3).锁消除

对不会逃逸到其他线程的变量执行加锁的操作可以删除加锁

示例:StringBuffer局部变量在一个线程中多次append(局部变量属于虚拟机栈是线程私有的)

public class Test {
    public static void main(String[] args) {
        StringBuffer sb=new StringBuffer();
        sb.append("1");
        sb.append("2");
        sb.append("3");
    }
}

2.常见的锁策略及CAS

多线程中锁类型的划分

API层面:synchronized加锁 Lock加锁

锁的类型:偏向锁轻量级锁重量级锁自旋锁独占锁共享锁公平锁非公平锁等等

<1>.乐观锁和悲观锁

乐观锁和悲观锁的设计思想(和语言是无关的不是java多线程独有的)

根据使用常见来阐述:

乐观锁:同一个时间点经常只有一个线程来操作共享变量适合使用乐观锁

悲观锁:同一个时间点经常有多个线程来操作共享变量适合使用悲观锁

乐观锁的实现原理

通过直接操作共享变变量(不会阻塞)通过调用的api的返回值来知道操作是成功还是失败的 java多线程的实现:基于CAS的方式实现(Compare and Swap)

令:主存中需要操作的变量为V线程A的工作内存中读入A修改为N

有另一个线程可能对主存中的V进行操作

此时:新的主存中操作的变量令为O比较线程A中的V和此时主存中的O是否相等如果相等说明可以将N写回主存如果不相等任务主存中的变量被B线程操作过此时A中的N不写入主存线程A不做任何事情

在这里插入图片描述

悲观锁的实现原理:类似于synchronized加锁方式

**CAS中可能存在的问题(ABA问题) **

肯主存中原来的V值被线程B加一再减一依然满足上述线程A可以写入N的条件

解决办法:为主存中的变量加上一个版本好在上诉A线程可写入的基础上再比较一次版本好即可解决

CAS在java中是使用unsafe类来完成的本质上是基于CPU提供的对变量原子性线程安全的修改操作

<2>自旋锁

按照普通加锁的方式处理当线程在抢锁失败之后会进入阻塞状态放弃CPU需要经过很久才能被再次调度所以引入读写锁当锁竞争失败之后只需要很短时间锁就能再次被释放此时让竞争失败的线程进入自旋不在用户态和内核态之间切换只要没抢到锁就死等

类似以下代码

<1>.无条件的自选:

while(抢锁(lock)==失败{}

自旋锁的缺陷:如果之前的假设(锁很快就能被释放)没有满足那么进入自旋的线程就一直在消耗CPU的资源长期在做无用功

<2>.有条件的自旋:

如可中断的自旋:自旋时线程判断中断标志位后再执行或者限制自旋的次数限制自旋的时间

自旋锁悲观乐观锁CAS的总结

<1>.悲观锁是线程先加锁之后再修改变量的操作

<2>.乐观锁是线程直接尝试修改变量(不会阻塞)在java多线程中是基于CAS 实现的

<3>.CAS

概念:Compare and Swap比较并交换

实现/原理:基于unsafe来实现本质上是基于CPU提供的接口保证线程安全修改变量

使用(V,O,N):V为内存地址中存放的实际值O为预期的值(旧值)N为更新的值(新值)

可能出现的问题:ABA问题(引入版本号解决)

<4>.自旋+CAS

适用的场景:同一个时间点常常只有一个线程进行操作

不适应的场景

1.同一个时间点常常有多个线程进行操作

2.CAS的操作时间时间太长给了其他线程操作共享变量的机会那么CAS的成功率会很低经常做无用功

自旋的缺陷:线程一直处于运行态会很耗费CPU的资源

<3>可重入锁

允许同一个线程多次获取同一把锁

java中只要以Reentrant开头命名的锁都是可重入的锁现有的jdk提供的lock的实现类和synchronized加锁,都是可重入锁例如:

public class Test2 {
    public static synchronized void t1(){
        t2();
    }
    public static synchronized void t2(){
    }
    public static void main(String[] args) {
        t1();
    }
}

3.Lock体系

在这里插入图片描述

<1>Lock接口

(1)使用Lock锁实现线程同步

上代码!

public class AccountRunnable implements  Runnable {
    private Account account = new Account();
    //买一把锁
    Lock lock = new ReentrantLock(); //Re-entrant-Lock  可重入锁
    @Override
    public void run() {
        //此处省略300句
        try{
//上锁
            lock.lock();
            //判断余额是否足够够取之不够不取之
            if(account.getBalance()>=400){
                try {
                    Thread.sleep(1);
                } catch (InterruptedException e) {
                    e.printStackTrace();
                }
                method1();
                //取之
                account.withDraw(400);
                //输出信息
                System.out.println(Thread.currentThread().getName()+
                   "取款成功现在的余额是"+account.getBalance());
            }else{
                 System.out.println("余额不足"+Thread.currentThread().getName()
                 +"取款失败现在的余额是"   +account.getBalance());
            }
        }finally {
            //解锁
            lock.unlock();
        }
        //此处省略100句
    }
}

这里要注意:释放锁时要考虑是否出现异常和上面synchronized加锁相同要进行两次锁释放这里将锁放在finally代码块中

(2)Lock加锁的四种方式

形象记忆:男生追女生

1.lock():一直表白直到成功

lock()方法是平常使用得最多的一个方法就是用来获取锁如果锁已被其他线程获取则进行等待

2.tryLock():表白一次失败就放弃

tryLock()方法是有返回值的它表示用来尝试获取锁如果获取成功则返回true如果获取失败(即锁已被其他线程获取)则返回false也就说这个方法无论如何都会立即返回拿不到锁时不会一直在那等待

3.tryLock(long time, TimeUnit unit) 在一定的时间内持续表白如果时间到了则放弃

tryLock(long time, TimeUnit unit)方法和tryLock()方法是类似的只不过区别在于这个方法在拿不到锁时会等待一定的时间在时间期限之内如果还拿不到锁就返回false如果如果一开始拿到锁或者在等待期间内拿到了锁则返回true

4.lockInterruptibly()  

一直表白当被通知她有男朋友了才放弃 lockInterruptibly()方法比较特殊当通过这个方法去获取锁时如果线程正在等待获取锁则这个线程能够响应中断即中断线程的等待状态

也就使说当这个线程使用lockInterruptibly()获取锁当被interrupt中断时才会停止竞争锁

<2>AQS简单认识

AQS: AbstractQuenedSynchronizer抽象的队列式同步器是除了java自带的synchronized关键字之外的锁机制这个类在java.util.concurrent.locks包.

AQS的核心思想是: 如果被请求的共享资源空闲则将当前请求资源的线程设置为有效的工作线程并将共享资源设置为锁定状态如果被请求的共享资源被占用那么就需要一套线程阻塞等待以及被唤醒时锁分配的机制这个机制AQS是用CLH队列锁实现的即将暂时获取不到锁的线程加入到队列中

AQS的实现方式

在这里插入图片描述

如图示AQS维护了一个volatile int state和一个FIFO线程等待队列多线程争用资源被阻塞的时候就会进入这个队列state就是共享资源

AQS 定义了两种资源共享方式

1.Exclusive:独占只有一个线程能执行如ReentrantLock

2.Share:共享多个线程可以同时执行如Semaphore、CountDownLatch、ReadWriteLockCyclicBarrier

<3>ReentrantLock

(1)ReentrantLock基本概念

ReentrantLock意思是“可重入锁”ReentrantLock是唯一实现了Lock接口的非内部类并且ReentrantLock提供了更多的方法

ReentrantLock锁在同一个时间点只能被一个线程锁持有

ReentraantLock是通过一个FIFO的等待队列来管理获取该锁所有线程的在“公平锁”的机制下线程依次排队获取锁而“非公平锁”在锁是可获取状态时不管自己是不是在队列的开头都会获取锁

当单个线程或线程交替执行时他与队列无关只会在jdk级别解决性能高

(2)自己实现一个简单的ReentrantLock

原理:自旋+park–unpark+CAS

public class Test2 {
    volatile int status=0;
    Queue parkQueue;//集合 数组  list
    void lock(){
        while(!compareAndSet(0,1)){
            //这里不能用sleep或yield实现
            //sleep无法确定睡眠的时间
            //yield只能用于两个线程竞争当有多个线程之后t1抢不到锁yield会让出cpu但是可能下一次cpu还是调t1
            park();
        }
        unlock();
    }
    void unlock(){
        lock_notify();
    }
    void park(){
        //将当期线程加入到等待队列
        parkQueue.add(currentThread);
        //将当期线程释放cpu  阻塞   睡眠
        releaseCpu();
    }
    void lock_notify(){
        //status=0
        //得到要唤醒的线程头部线程
        Thread t=parkQueue.header();
        //唤醒等待线程
        unpark(t);
    }
}
(3)ReentrantLock部分源码分析

ReentrantLock锁分为公平锁和非公平锁(创建不加参数时默认非公平锁)

ReentrantLock提供了两个构造器

//非公平锁
 public ReentrantLock() {
        sync = new NonfairSync();
    }
//公平锁
 public ReentrantLock(boolean fair) {
        sync = fair ? new FairSync() : new NonfairSync();
    }

ReentrantLock的lock方式

在这里插入图片描述

非公平锁

调用lock方法:

final void lock() {
    if (compareAndSetState(0, 1))//首先用一个CAS操作判断state是否是0(表示当前锁未被占用)
        setExclusiveOwnerThread(Thread.currentThread());//设置当前占有锁的线程为该线程
    else
        acquire(1);
}

首先用一个CAS操作判断state是否是0(表示当前锁未被占用)如果是0则把它置为1并且设置当前线程为该锁的独占线程表示获取锁成功当多个线程同时尝试占用同一个锁时CAS操作只能保证一个线程操作成功剩下的只能乖乖的去排队

“非公平”即体现在这里如果占用锁的线程刚释放锁state置为0而排队等待锁的线程还未唤醒时新来的线程就直接抢占了该锁那么就“插队”了

下面说说acquire的过程

public final void acquire(int arg) {
    //首先看看自己要不要排队如果不用排队获取锁要排队加入AQS队列 
    if (!tryAcquire(arg) &&
        acquireQueued(addWaiter(Node.EXCLUSIVE), arg))
        selfInterrupt();
}

(1)尝试去获取锁(看看自己要不要排队)

非公平锁tryAcquire的流程是:检查state字段若为0表示锁未被占用那么尝试占用若不为0检查当前锁是否被自己占用若被自己占用则更新state字段表示重入锁的次数如果以上两点都没有成功则获取锁失败返回false

tryAcquire(arg)
final boolean nonfairTryAcquire(int acquires) {
    //获取当前线程
    final Thread current = Thread.currentThread();
    //获取state变量值
    int c = getState();
    if (c == 0) { //没有线程占用锁
        if (compareAndSetState(0, acquires)) {
            //占用锁成功,设置独占线程为当前线程
            setExclusiveOwnerThread(current);
            return true;
        }
    } else if (current == getExclusiveOwnerThread()) { //当前线程已经占用该锁 重入锁
        int nextc = c + acquires;
        if (nextc < 0) // overflow
            throw new Error("Maximum lock count exceeded");
        // 更新state值为新的重入次数
        setState(nextc);
        return true;
    }
    //获取锁失败
    return false;
}

(2)入队根据java运算符短路如果不需要排队方法直接返回如果需要排队进入addWaiter方法

公平锁

公平锁和非公平锁不同之处在于公平锁在获取锁的时候不会先去检查state状态而是直接执行aqcuire(1)

<4>ReadWriteLock锁

ReadWriteLock也是一个接口在它里面只定义了两个方法:

public   interface   ReadWriteLock { 
      Lock readLock();   
      Lock writeLock(); 
} 

一个用来获取读锁一个用来获取写锁也就是说将文件的读写操作分开分成2个锁来分配给线程从而使得多个线程可以同时进行读操作

ReadWriteLock是一个接口ReentrantReadWriteLock是它的实现类该类中包括两个内部类ReadLock和WriteLock这两个内部类实现了Lock接口

认识ReadWriteLock锁

public class TestLock {
    public static void main(String[] args) {
//默认也是非公平锁  也是可重入锁
        ReadWriteLock rwl = new ReentrantReadWriteLock();
        //多次返回的都是同一把读锁 同一把写锁
        Lock readLock = rwl.readLock();
        Lock readLock2 = rwl.readLock();
        Lock writeLock = rwl.writeLock();
        readLock.lock();
        readLock.unlock();
        System.out.println(readLock==readLock2);
    }
}

注意:从结果中看到从一个ReadWriteLock中多次获取的ReadLock、WriteLock是同一把读锁同一把写锁

4.Lock锁和同步锁(synchronized)的区别

在这里插入图片描述

5.死锁

先上代码:

package threadadvanced.lesson1;
class Pen {
	private String pen = "笔" ; 
	public String getPen() {
		return pen;
	}
}
class Book {
	private String book = "本" ; 
	public String getBook() {
		return book;
	}
}
public class DeadLock {
	private static Pen pen = new Pen() ; 
	private static Book book = new Book() ; 
	public static void main(String[] args) {
		new DeadLock().deadLock();
	}
	public void deadLock() {
		Thread thread1 = new Thread(new Runnable() { // 笔线程
			@Override
			public void run() {
				synchronized (pen) {
					try {
						Thread.sleep(1000);
					} catch (InterruptedException e) {
						e.printStackTrace();
					}
					System.out.println(Thread.currentThread()+" :我有笔我就不给你");
					synchronized (book) {
						System.out.println(Thread.currentThread()+" :把你的本给我!");
					}
				}
			}
		},"Pen") ; 
		
		Thread thread2 = new Thread(new Runnable() { // 本子线程
			@Override
			public void run() {
				synchronized (book) {
					System.out.println(Thread.currentThread()+" :我有本子我就不给你!");
					synchronized (pen) {
						System.out.println(Thread.currentThread()+" :把你的笔给我!");
					}
				}
				
			}
		},"Book") ; 
		thread1.start();
		thread2.start();
	}
}

出现死锁:

在这里插入图片描述

jconsole检查死锁:

在这里插入图片描述

1.死锁出现的原因:

至少两个线程互相持有对方需要的资源没有释放再次申请对方以及持有的资源

2.出现死锁的后果:

线程互相阻塞等待地方的资源会一直处于阻塞等待的状态

3.如何检测死锁:

使用jdk工具:jconsole(查看线程)---->jstack

4.解决死锁的方法:

(1)资源一次性分配(破坏请求与保持条件)

(2)在满足一定条件的时候主动释放资源

(3)资源的有序分配:系统为每一类资源赋予一个编号每个线程按照编号递请求资源释放则相反

七.多线程案例

1.生产者消费者问题

示例

面包店

10个生产者每个每次生产3个

20个消费者每个每次消费一个

进阶版需求

面包师傅每个最多生产30次面包店每天生产10303=900个面包

消费者也不是一直消费把900个面包消费完结束

隐藏信息:面包店每天生产面包的最大数量为900个

消费者把900个面包消费完结束

代码示例

/**
 * 面包店
 * 10个生产者每个每次生产3个
 * 20个消费者每个每次消费一个
 *
 * 进阶版需求
 * 面包师傅每个最多生产30次面包店每天生产10*30*3=900个面包
 * 消费者也不是一直消费把900个面包消费完结束
 *
 * 隐藏信息:面包店每天生产面包的最大数量为900个
 *            消费者把900个面包消费完结束
 */
public class AdvancedBreadShop {
    //面包店库存数
    private static int COUNT;
    //面包店生产面包的总数,不会消费的
    private static int PRODUCE_NUMBER;
    public static class Consumer implements Runnable{
        private String name;
        public Consumer(String name) {
            this.name = name;
        }
        @Override
        public void run() {
            try {
                while (true){
                    synchronized (AdvancedBreadShop.class){
                        if(PRODUCE_NUMBER==900&&COUNT==0){
                            System.out.println("今天面包已经卖完了");
                            break;
                        }else {
                            if(COUNT==0){
                                AdvancedBreadShop.class.wait();
                            }else {
                                System.out.printf("%s消费了一个面包\n",this.name);
                                COUNT--;
                                AdvancedBreadShop.class.notifyAll();
                                Thread.sleep(100);
                            }
                        }
                    }
                    Thread.sleep(100);
                }
            } catch (InterruptedException e) {
                e.printStackTrace();
            }
        }
    }
    private static class Producer implements Runnable{
        private String name;
        public Producer(String name) {
            this.name = name;
        }
        @Override
        public void run() {
            try {
                //生产者生产30次结束循环
                for(int i=0;i<=30;i++) {
                    synchronized (AdvancedBreadShop.class){
                        if(i==30){
                            System.out.println("今天面包生产完了");
                            break;
                        }else {
                            if(COUNT>97){
                                AdvancedBreadShop.class.wait();
                            }else {
                                COUNT=COUNT+3;
                                PRODUCE_NUMBER=PRODUCE_NUMBER+3;
                                System.out.printf("%s生产了三个面包\n",this.name);
                                AdvancedBreadShop.class.notifyAll();
                                Thread.sleep(100);
                            }
                        }
                    }
                    Thread.sleep(100);
                }
            } catch (InterruptedException e) {
                e.printStackTrace();
            }
        }
    }
    public static void main(String[] args) {
        Thread[] Consumers=new Thread[20];
        Thread[] Producers=new Thread[10];
        for (int i = 0; i <20 ; i++) {
            Consumers[i]=new Thread(new Consumer(String.valueOf(i)));
        }
        for (int i = 0; i <10 ; i++) {
            Producers[i]=new Thread(new Producer(String.valueOf(i)));
        }
        for (int i = 0; i <20 ; i++) {
            Consumers[i].start();
        }
        for (int i = 0; i <10 ; i++) {
            Producers[i].start();
        }
    }
}

2.单例模式

基于单例模式下的懒汉模式(双重校验锁实现)(多线程版二次判断效率高)代码示例:

public class Singleton {
    //volatile关键字修饰保证的可见性和代码的顺序性
    private static volatile Singleton instance = null;
    private Singleton() {
    }
    public static Singleton getInstance() {
        //判断instance是否为空竞争锁的条件
        if (instance == null) {
            //保证线程安全为Singleton.class加锁
            synchronized (Singleton.class) {
                //再次判断instance是否为空防止多个线程进入第一个if后
                //对synchronized锁竞争失败进入阻塞状态后再次进入运行态时
                //new了多个Singleton不符合单例模式
                //保证线程安全
                if (instance == null) {
                    instance = new Singleton();
                }
            }
        }
        return instance;
        }
        }

3.阻塞式队列

生产者消费者模式就是通过一个容器来解决生产者和消费者的强耦合问题生产者和消费者彼此之间不直接通讯而通过阻塞队列来进行通讯所以生产者生产完数据之后不用等待消费者处理直接扔给阻塞队列消费者不找生产者要数据而是直接从阻塞队列里取阻塞队列就相当于一个缓冲区平衡了生产者和消费者的处理能力这个阻塞队列就是用来给生产者和消费者解耦的

阻塞式队列代码实现:

/**
 * 实现阻塞队列
 * 1.线程安全问题:在多线程情况下puttake不具有原子性4个属性不具有可见性
 * 2.put操作:如果存满了需要阻塞等待take操作:如果是空阻塞等待
 * @param <T>
 */
public class MyBlockingQueue <T>{
    //使用数组实现循环队列
    private Object[] queue;
    //存放元素的索引
    private int putIndex ;
    //取元素的索引
    private int takeIndex;
    //当前存放元素的数量
    private int size;
    public MyBlockingQueue(int len){
        queue=new Object[len];
    }
    //存放元素需要考虑:
    //1.putIndex超过数组长度
    //2.size达到数组最大长度
    public synchronized void put(T e) throws InterruptedException {
        //不满足执行条件时一直阻塞等待
        //当阻塞等待都被唤醒并再次竞争成功对象锁回复往下执行时条件可能被其他线程修改
        while (size==queue.length){
            this.wait();
        }
        //存放到数组中放元素的索引位置
        queue[putIndex]=e;
        putIndex=(putIndex+1)%queue.length;
        size++;
        notifyAll();
    }
    //取元素
    public synchronized T take() throws InterruptedException {
       while (size==0){
            this.wait();
        }
        T t= (T) queue[takeIndex];
        queue[takeIndex]=null;
        takeIndex=(takeIndex+1)%queue.length;
        size--;
        notifyAll();
        return t;
    }
    public int size(){
        return size;
    }
    public static void main(String[] args) {
        MyBlockingQueue<Integer>queue=new MyBlockingQueue<>(10);
        //多线程的调试方式:1.写打印语句 2.jconsole
        for (int i = 0; i <3 ; i++) {
            new Thread(new Runnable() {
                @Override
                public void run() {
                    try {
                        for (int j = 0; j <100 ; j++) {
                            queue.put(j);
                        }
                    } catch (InterruptedException e) {
                        e.printStackTrace();
                    }
                }
            }).start();
        }
        for (int i = 0; i <3 ; i++) {
            new Thread(new Runnable() {
                @Override
                public void run() {
                    try {
                       while (true){
                          int t= queue.take();
                           System.out.println(Thread.currentThread().getName()+":"+t);
                       }
                    } catch (InterruptedException e) {
                        e.printStackTrace();
                    }
                }
            }).start();
        }
    }
}

4.线程池

线程池最大的好处就是减少每次启动、销毁线程的损耗

import java.util.concurrent.*;
public class ThreadPoolExecutorTest {
    public static void main(String[] args) {
        //以快递公司快递员快递业务为模型
        ThreadPoolExecutor pool=new ThreadPoolExecutor(
                5,//核心线程数---->正式员工数
                10,//最大线程数-->正式员工+临时员工
                60,//临时工的最大等待时间
                TimeUnit.SECONDS,//idle线程的空闲时间-->临时工最大的存活时间超过就解雇
                new LinkedBlockingQueue<>(),//阻塞队列任务存放的地方--->快递仓库
                new ThreadFactory() {
                    @Override
                    public Thread newThread(Runnable r) {
                        return new Thread(new Runnable() {
                            @Override
                            public void run() {
                                //r对象是线程池内部封装过的工作任务类(Worker)会一直循环等待的方式从阻塞队列中拿取任务并执行
                                //所以不能调用r.run();方法
                                System.out.println(Thread.currentThread().getName()+"开始执行了");
                            }
                        });
                    }
                },//创建线程的工厂类  线程池创建线程时调用该工厂类的方法创建线程(满足该工厂创建线程的要求)
                   //---->对应招聘员工的标准
                /**
                 * 拒绝策略:达到最大线程数且阻塞队列已满采取拒绝策略
                 * AbortPolicy:直接抛出RejectedExecutionException(不提供handler时的默认策略)
                 * CallerRunsPolicy:谁(某个线程)交给我(线程池)的任务我拒绝执行由谁自己去执行
                 * DiscardPolicy:交给我的任务直接丢弃掉
                 * DiscardOldestPolicy:阻塞队列中最旧的任务丢弃
                 */
                new ThreadPoolExecutor.AbortPolicy()//拒绝策略-->达到最大线程数且阻塞队列已满采取的拒绝策略
        );//线程池创建以后只要有任务们就会自动执行
        for (int i = 0; i <20 ; i++) {
            //线程池执行任务:execute方法submit方法--->提交执行一个任务
            //区别:返回值不同
            pool.execute(new Runnable() {
                @Override
                public void run() {
                    try {
                        Thread.sleep(1000);
                    } catch (InterruptedException e) {
                        e.printStackTrace();
                    }
                }
            });
        }
        //线程池有4个快捷的创建方式(实际工作不使用作为面试了解)
        //实际工作需要使用ThreadPoolExecutor构造参数是我们自己指定比较灵活
        ExecutorService pool2=Executors.newSingleThreadExecutor();//创建单线程池
        ExecutorService pool3=Executors.newCachedThreadPool();//缓存的线程池
        ExecutorService pool5=Executors.newFixedThreadPool(4);//固定大小线程池
        ScheduledExecutorService pool4=Executors.newScheduledThreadPool(4);//计划任务线程池
        //两秒中之后执行这个任务
        pool4.schedule(new Runnable() {
            @Override
            public void run() {
                System.out.println("hello");
            }
        }, 2, TimeUnit.SECONDS);
        //一直执行任务
        pool4.scheduleAtFixedRate(new Runnable() {
            @Override
            public void run() {
                System.out.println("hello");
            }
        }, 2, 1,TimeUnit.SECONDS);//比如一个脑子两秒后开始叫我然后每隔一秒叫我一次
    }
}

八.总结

(1)代码块锁是一个防止数据发生错误的一个重要手段

(2)对象的统一性是非常重要的这要想到对象的传入问题要操作的对象只能new一次其他的操作都是对这个传入的对象进行的才能保证数据一致性完整性和正确性


相关文章

猜您喜欢

  • SSM框架原理 SSM框架流程及原理分析

    想了解SSM框架流程及原理分析的相关内容吗AAAhxz在本文为您仔细讲解SSM框架原理的相关知识和一些Code实例欢迎阅读和指正我们先划重点:SSM框架原理,SSM框架原理及流程下面大家一起来学习吧..
  • Java行为型模式 Java设计模式之初识行为型模式

    想了解Java设计模式之初识行为型模式的相关内容吗三笠·阿卡曼在本文为您仔细讲解Java行为型模式的相关知识和一些Code实例欢迎阅读和指正我们先划重点:Java行为型模式,java设计模式下面大家一起来学习吧..

网友评论

Copyright 2020 www.fresh-weather.com 【世纪下载站】 版权所有 软件发布

声明:所有软件和文章来自软件开发商或者作者 如有异议 请与本站联系 点此查看联系方式