线程详解

1、线程简介

想学习线程的友友建议可以用b站看狂神的视频,博主大力推荐;

视频地址:https://www.bilibili.com/video/BV1V4411p7EF?p=1&vd_source=baa7aeee4e9189a4f5dfc295165034fe

觉得视频不错可以三连关注走一波嗷!不要忘记给狂神支持!

进程、线程、多线程;

进程

进程就是执行程序的一次执行过程,一个程序执行时,便是启动了一个进程,进程是系统分配资源的单位;

进程是一个动态的概念,程序本身并不是进程,而在操作系统中运行的程序被叫做进程!!!

线程

一个进程中包含了若干个线程,线程是CPU调度和执行的单位;

多线程

多线程就是指多个线程;

2、线程创建

2.1、线程的创建方式(三种)

一说有四种,最后一种说是线程池,但其实也是通过通过实现Runnable或者Callable接口来实现的,究其底层其实也就是实现重写run方法;

1、继承Thread类

  • 自定义线程类继承Thread类;

  • 重写run()方法,编写线程执行体;

  • 创建线程对象,调用start()方法启动线程;

package com.newThread.demo01;

/**
 * 线程交替执行,看CPU的调度处理
 * 一定得记得调用start方法
 */
public class TestThread1 extends Thread{
    // 重写 run 方法
    @Override
    public void run(){
        for (int i = 0; i < 200; i++) {
            System.out.println("多线程交替执行测试----"+ i );
        }
    }

    public static void main(String[] args) {
        // new 一个线程
        TestThread1 testThread1 = new TestThread1();

        // 调用新建的线程
        testThread1.start();

        for (int i = 0; i < 1000; i++) {
            System.out.println("主线程执行一次-----" + i);
        }
    }

}

测试结果:

在这里插入图片描述

案例(网图下载)

通过 commons-io 的jar包,下载网上的图片(通过URL);

(1)导入下载的包;

通过Maven调用 commons-io 的包;

<dependency>
    <groupId>commons-io</groupId>
    <artifactId>commons-io</artifactId>
    <version>2.8.0</version>
</dependency>
(2)编写下载器以及重写thread类;
package com.zeiyalo.demo01;

import org.apache.commons.io.FileUtils;

import java.io.File;
import java.io.IOException;
import java.net.URL;

/**
 * @author Zeiyalo
 */
public class TestThread2 extends Thread{
    private String url;
    private String name;

    public TestThread2(String url, String name) {
        this.url = url;
        this.name = name;
    }

    @Override
    public void run(){
        WebDownloader webDownloader = new WebDownloader();
        webDownloader.downloader(this.url, this.name);
        System.out.println("图片下载完成!");
    }
}

/**
 * 下载器
 */
class WebDownloader{
    public void downloader(String url, String name){
        try {
            FileUtils.copyURLToFile(new URL(url), new File(name));
        } catch (IOException e) {
            e.printStackTrace();
            System.out.println("IO异常,downloader方法出现问题");
        }

    }
}
(3)编写测试类;
import com.zeiyalo.demo01.TestThread2;
import org.junit.Test;

public class MyTest {
    @Test
    public void TestThread2(){
        TestThread2 t1 = new TestThread2("https://csdnimg.cn/release/blogv2/dist/pc/img/newHeart2023Active.png","stared.png");
        TestThread2 t2 = new TestThread2("https://csdnimg.cn/release/blogv2/dist/pc/img/newHeart2023Black.png","star.png");
        t1.start();
        t2.start();
    }
}

(4)测试结果;

选了CSDN上的两中点赞图标进行测试;

并不能使用测试类进行下载,控制台未提示我图片下载完成,只提示测试通过;

所以改用psvm主程序调用方式进行改进,完成下载;

import com.zeiyalo.demo01.TestThread2;
import org.junit.Test;

public class MyTest {
    // 测试失败
    @Test
    public void TestThread2(){
        TestThread2 t1 = new TestThread2("https://csdnimg.cn/release/blogv2/dist/pc/img/newHeart2023Active.png","stared.png");
        TestThread2 t2 = new TestThread2("https://csdnimg.cn/release/blogv2/dist/pc/img/newHeart2023Black.png","star.png");
        t1.start();
        t2.start();
    }

    // 使用主程序调用,成功
    public static void main(String[] args) {
        TestThread2 t1 = new TestThread2("https://csdnimg.cn/release/blogv2/dist/pc/img/newHeart2023Active.png","stared.png");
        TestThread2 t2 = new TestThread2("https://csdnimg.cn/release/blogv2/dist/pc/img/newHeart2023Black.png","star.png");
        t1.start();
        t2.start();
    }
}

在这里插入图片描述

案例完成;

2、实现Runnable接口;

package com.zeiyalo.demo01;

/**
 * @author Zeiyalo
 */
public class MyRunnable implements Runnable{
    @Override
    public void run() {
        for (int i = 0; i < 200; i++) {
            System.out.println("多线程交替执行测试----"+ i );
        }
    }

    public static void main(String[] args) {
        MyRunnable myRunnable = new MyRunnable();

        Thread thread = new Thread(myRunnable);
        thread.start();

        for (int i = 0; i < 1000; i++) {
            System.out.println("主线程执行一次-----" + i);
        }

    }
}

实现了runnable接口后一般使用一个线程来调用runnable接口的实现类;

案例(卖票、卖票实现,会遇到并发抢资源问题,可以通过加锁解决!)

设计一个卖票线程,然后几个线程去抢票;

package com.zeiyalo.demo01;

/**
 * @author Zeiyalo
 */
public class TicketRunnable implements Runnable {
    // 票数
    private int num = 10;

    @Override
    public void run() {
        while (true) {
            if (num <= 0) {
                break;
            }
            System.out.println(Thread.currentThread().getName() + "抢到了第" + num + "张票!");
            num--;
        }
    }

    public static void main(String[] args) {
        // 卖票线程
        TicketRunnable runnable = new TicketRunnable();

        // 多个抢票线程
        new Thread(runnable, "小明").start();
        new Thread(runnable, "大红").start();
        new Thread(runnable, "梁江").start();
        new Thread(runnable, "余华").start();

    }

    /**
     * 测试结果:
     * 小明抢到了第10张票!
     * 余华抢到了第10张票!
     * 余华抢到了第8张票!
     * 余华抢到了第7张票!
     * 余华抢到了第6张票!
     * 余华抢到了第5张票!
     * 余华抢到了第4张票!
     * 大红抢到了第10张票!
     * 梁江抢到了第10张票!
     * 梁江抢到了第1张票!
     * 大红抢到了第2张票!
     * 余华抢到了第3张票!
     * 小明抢到了第9张票!
     *
     * 上面的例子中,由于卖票线程未加锁,所以会导致一张票被多个其他线程抢到,这是一个多线程并发问题,可以通过加锁来实现对资源的更	 * 好的分配;
     */

}

案例完成,出现并发问题;

小结:

  • 不建议使用继承Thread类方法,因为OPP单继承的局限性;
  • 建议使用实现Runnable接口方法,可以避免单继承的局限性,比较灵活方便,同一个对象可以被多个线程使用;
  • 两者都具有多线程能力,但是第一种(继承)的启动线程的方法是"子类对象.start()“,第二种方法(实现接口)的启动方法是"传入目标对象(接口实现类) + new Thread(目标对象).start()”;

案例(龟兔赛跑)

乌龟和兔子赛跑,兔子中间睡觉导致乌龟赢得比赛;

解决思路:

希望自己设计一个参赛选手线程,两个参赛选手赛程来争抢比赛距离线程,但暂时知识还不够实现;

狂神说的思路有问题,因为他的思路中,兔子和乌龟是一人走一步,最终两者相加为一百,所以说思路错了,应该是各自走一段相等距离,谁先走到谁算赢;

这个问题的本质就是锻炼使用 sleep() 方法,不必过多纠结;

3、实现Callable接口(了解);

通过之前实现的图片下载类来进行改造;

实现步骤

(1)实现Callable接口;

public class MyCallable implements Callable<Boolean> {

(2)重写call方法;

	@Override
    public Boolean call() throws Exception {
        WebDownloader webDownloader = new WebDownloader();
        webDownloader.downloader(this.url, this.name);
        System.out.println("图片 " + Thread.currentThread().getName() + " 下载完成!");
        return true;
    }

(3)创建执行服务;

// 创建执行服务
ExecutorService service = newFixedThreadPool(2);

(4)提交执行;

// 提交执行
Future<Boolean> r1 = service.submit(c1);
Future<Boolean> r2 = service.submit(c2);

(5)获取结果;

// 获取结果
System.out.println(r1.get());
System.out.println(r2.get());

(6)关闭服务;

// 关闭服务
service.shutdown();

最终代码

package com.zeiyalo.demo02;

import org.apache.commons.io.FileUtils;

import java.io.File;
import java.io.IOException;
import java.net.URL;
import java.util.concurrent.Callable;
import java.util.concurrent.ExecutionException;
import java.util.concurrent.ExecutorService;
import java.util.concurrent.Future;

import static java.util.concurrent.Executors.*;


/**
 * @author Zeiyalo
 */
public class MyCallable implements Callable<Boolean> {
    private String url;
    private String name;

    public MyCallable(String url, String name) {
        this.url = url;
        this.name = name;
    }

    @Override
    public Boolean call() throws Exception {
        WebDownloader webDownloader = new WebDownloader();
        webDownloader.downloader(this.url, this.name);
        System.out.println("图片 " + Thread.currentThread().getName() + " 下载完成!");
        return true;
    }

    public static void main(String[] args) {
        MyCallable c1 = new MyCallable("https://csdnimg.cn/release/blogv2/dist/pc/img/newHeart2023Active.png","stared.png");
        MyCallable c2 = new MyCallable("https://csdnimg.cn/release/blogv2/dist/pc/img/newHeart2023Black.png","star.png");

        // 创建执行服务
        ExecutorService service = newFixedThreadPool(2);

        // 提交执行
        Future<Boolean> r1 = service.submit(c1);
        Future<Boolean> r2 = service.submit(c2);


        try {
            // 获取结果
            System.out.println(r1.get());
            System.out.println(r2.get());
        } catch (InterruptedException e) {
            e.printStackTrace();
        } catch (ExecutionException e) {
            e.printStackTrace();
        } finally {
            // 关闭服务
            service.shutdown();
        }

    }
}

/**
 * 下载器
 */
class WebDownloader{
    public void downloader(String url, String name){
        try {
            FileUtils.copyURLToFile(new URL(url), new File(name));
        } catch (IOException e) {
            e.printStackTrace();
            System.out.println("IO异常,downloader方法出现问题");
        }

    }
}

测试结果如下:

在这里插入图片描述

额外探究图片名称

在这里插入图片描述

在原来的call方法中加上这样一句话,两者是否有区别;

测试结果:

在这里插入图片描述

说明getName() 的取值在有自己的变量name时仍取线程池中的名;

Callable与前两种方式的对比

  • 使用前需要开启线程池;
  • 需要使用服务来提交服务执行;
  • 有返回值;
  • 使用完需要关闭服务;

3、静态代理

案例(结婚案例)

你结婚时,找婚庆公司,婚庆公司就是一个代理!

package com.zeiyalo.proxy;

/**
 * 静态代理:
 * 1、将一些琐事交给外层的代理对象去处理,自己本身的真实对象只需要专注与最重要的事就够了;
 * 2、在线程中也用到了类似的思想,处理对应业务的线程只需要去关注于业务,其他的琐事交给代理对象也就是外层线程处理就好了;
 * 3、其实也有着加一层和AOP的思想在其中;
 */

/**
 * @author Zeiyalo
 */
public class StaticProxy {
    
    public static void main(String[] args) {
        new WeddingCorp(new You()).happyMarry();    
    }
}


interface Marry{

    void happyMarry();

}

/**
 * 真实结婚对象
 */
class You implements Marry{

    @Override
    public void happyMarry() {
        System.out.println("结婚ing,很开心!");
    }
}

/**
 * 婚庆公司,代理对象
 */
class WeddingCorp implements Marry{

    private Marry targetObj;

    public WeddingCorp(Marry targetObj) {
        this.targetObj = targetObj;
    }

    @Override
    public void happyMarry() {
        before();
        targetObj.happyMarry();
        after();
    }

    private void after() {
        System.out.println("婚礼结束,收尾款!!");
    }

    private void before() {
        System.out.println("婚礼准备,筹划,准备举办!!");
    }
}

总结:

  • 真实对象和代理对象都要实现一个接口;
  • 代理对象要代理真实角色,需要真实对象作为一个属性参数;
  • Runnable接口的实现就是类似与静态代理;

4、Lambda表达式

为了避免匿名内部类定义过多,于是Java推出了Lambda表达式(在Java 8之后才能使用),实质上属于是函数式编程的概念;

一般来说有三种表达形式:

- (params) -> expression[表达式]
- (params) -> statement[语句]
- (params) -> { statement }

在线程提到这个是因为可以直接使用Lambda表达式直接重写一些比较简单的 run() 方法,例如:

new Thread(() ->  System.out.println("这是一个线程!")).start();

Lambda表达式的优点

  • 避免匿名内部类定义过多;
  • 可以让你的代码更为简洁;
  • 去掉了一堆没有意义的代码;

Lambda一步步简化的过程:

package com.zeiyalo.lambda;

/**
 * @author Zeiyalo
 */
public class MyLambda {

    /**
     * 2、静态内部类
     * 通过静态内部类实现函数式接口
     */
    static class Like2 implements ILike {

        @Override
        public void lambda() {
            System.out.println("I like lambda 2");
        }
    }






    public static void main(String[] args) {
        /**
         * 1、实现类
         */
        ILike like1 = new Like1();
        like1.lambda();

        /**
         * 2、静态内部类
         */
        like1 = new Like2();
        like1.lambda();

        /**
         * 3、局部内部类
         */
        class Like3 implements ILike {

            @Override
            public void lambda() {
                System.out.println("I like lambda 3");
            }
        }

        like1 = new Like3();
        like1.lambda();

        /**
         * 4、匿名内部类
         * 可以直接省略内部类的名称,借助接口或者父类;
         */
        like1 = new ILike() {
            @Override
            public void lambda() {
                System.out.println("I like lambda 4");
            }
        };
        like1.lambda();

        /**
         * 5、lambda表达式
         * 将匿名内部类其他的无用信息也直接省略,进一步精简;
         */
        like1 = () -> {
            System.out.println("I like lambda 5");
        };
        like1.lambda();


    }
}

/**
 * 定义一个函数式接口(只有一个方法的接口叫函数式接口)
 */
interface ILike {

    void lambda();

}

/**
 * 1、实现类
 * 通过实现类来调用函数式接口;
 */
class Like1 implements ILike {

    @Override
    public void lambda() {
        System.out.println("I like lambda 1");
    }
}

简化lambda表达式:

package com.zeiyalo.lambda;

/**
 * @author Zeiyalo
 */
public class MyLambda1 {
    public static void main(String[] args) {
        ILove love;

        /**
         * 使用lambda表达式,前提时接口为函数式接口
         */
        love = (String a) -> {
            System.out.println("I love you," + a);
        };

        love.love("芳芳");

        /**
         * 参数类型可以去掉
         * 如果是多个参数,去掉时应该一起去掉参数类型;
         */
        love = a -> {
            System.out.println("I love you," + a);
        };

        love.love("芳芳");

        /**
         * 如果只有一行代码,可以去掉花括号,否则应该写在代码块中
         */
        love = a -> System.out.println("I love you," + a);

        love.love("芳芳");

    }
}

interface ILove {
    void love(String a);
}

class Love implements ILove{

    @Override
    public void love(String a) {
        System.out.println("I love you," + a);
    }
}

小结:

  • 首先使用lambda表达式的前提是为函数式接口;
  • 然后可以去掉入参的参数类型,一个入参可以去括号,如果是多个参数,去掉时应该一起去掉参数类型;
  • 最后如果只有一行代码,可以去掉花括号,否则应该写在代码块中;

5、线程状态

线程一般有五种状态;

在这里插入图片描述

源码里总共有六种:

  • **NEW:**线程刚刚创建,还未启动时的状态。
  • RUNNABLE:线程在JAVA虚拟机中执行的状态。
  • BLOCKED:线程被阻塞等待监视器锁定的状态。
  • WAITING: 线程无限期等待另一个线程执行特定操作的状态(需手动唤醒)。
  • TIMED_WAITING: 线程正在等待另一个线程执行最多等待时间的操作的状态(会自动唤醒)。
  • **TERMINATED:**线程已退出的状态。

线程停止

  • 建议线程正常停止 ——> 利用次数循环,不建议使用死循环;
  • 建议使用标志位 ——> 设置一个标志位;
  • 不建议使用stop或者destroy等JDK中已过时的方法;
package com.zeiyalo.state;

/**
 * @author Zeiyalo
 */
public class TestStop implements Runnable{

    private boolean flag = true;

    @Override
    public void run() {
        int i = 0;
        if (flag) {
            System.out.println("该线程正在运行---------" + i++);
        }
    }

    public void stop() {
        flag = false;
    }

    public static void main(String[] args) {
        TestStop testStop = new TestStop();
        new Thread(testStop).start();

        for (int i = 0; i < 1000; i++) {
            System.out.println("main 运行中" + i);
            if(i == 900) {
                testStop.stop();
                System.out.println("线程停止了---------------");
            }
        }
    }

}

线程休眠

通过sleep方法可以让线程进入休眠,到了规定时间在自动唤醒,线程进入 TIMED_WAITING 的状态;

1、通过线程休眠来做网络延时;

package com.zeiyalo.state;

import com.zeiyalo.demo01.TicketRunnable;

/**
 * @author Zeiyalo
 */
public class TestSleep implements Runnable {
    // 票数
    private int num = 10;

    @Override
    public void run() {
        while (true) {
            if (num <= 0) {
                break;
            }
            // 模拟延时
            try {
                Thread.sleep(10);
            } catch (InterruptedException e) {
                e.printStackTrace();
            }
            System.out.println(Thread.currentThread().getName() + "抢到了第" + num + "张票!");
            num--;
        }
    }

    public static void main(String[] args) {
        // 卖票线程
        TicketRunnable runnable = new TicketRunnable();

        // 多个抢票线程
        new Thread(runnable, "小明").start();
        new Thread(runnable, "大红").start();
        new Thread(runnable, "梁江").start();
        new Thread(runnable, "余华").start();

    }
}

2、通过线程休眠来做倒计时;

package com.zeiyalo.state;

/**
 * @author Zeiyalo
 */
public class TestSleep2 implements Runnable{

    /**
     * 倒计时
     */
    @Override
    public void run() {
        int num = 10;
        while (true) {
            System.out.println(num--);
            try {
                Thread.sleep(1000);
            } catch (InterruptedException e) {
                e.printStackTrace();
            }
            if (num<=0) {
                break;
            }
        }
    }

    public static void main(String[] args) {
        TestSleep2 sleep2 = new TestSleep2();
        new Thread(sleep2).start();
    }
}

3、通过线程休眠来获取当前系统时间;

package com.zeiyalo.state;

import java.text.SimpleDateFormat;
import java.util.Date;

/**
 * @author Zeiyalo
 */
public class TestSleep1 {
    /**
     * 打印系统当前时间
     */

    public static void main(String[] args) throws InterruptedException {
        int num = 10;
        Date date = new Date(System.currentTimeMillis());

        while (true) {
            System.out.println(new SimpleDateFormat("yyyy-MM-dd HH:mm:ss").format(date));
            Thread.sleep(1000);
            date = new Date(System.currentTimeMillis());
            if (num-- <= 0) {
                break;
            }
        }
    }

}

线程礼让——yield

  • 礼让线程,让当前的线程暂停,但不阻塞;
  • 将线程从运行状态转换为就绪状态;
  • 线程礼让不一定成功,主要看CPU的调度结果;
package com.zeiyalo.state;

/**
 * @author Zeiyalo
 */
public class TestYield {

    public static void main(String[] args) {
        MyYield myYield = new MyYield();

        new Thread(myYield, "a").start();
        new Thread(myYield, "b").start();
    }
}


class MyYield implements Runnable {

    @Override
    public void run() {
        System.out.println(Thread.currentThread().getName() + "线程开始执行!");
        Thread.yield();
        System.out.println(Thread.currentThread().getName() + "线程执行完毕!");
    }
}

线程强制执行——join

  • 使用Join后,其他线程阻塞,有限跑join线程;
  • 相当于插队;
package com.zeiyalo.state;

/**
 * @author Zeiyalo
 */
public class TestJoin implements Runnable{
    @Override
    public void run() {
        for (int i = 0; i < 1000; i++) {
            System.out.println("Join线程,需优先执行————————" + i);
        }
    }

    public static void main(String[] args) throws InterruptedException {
        TestJoin testJoin = new TestJoin();
        Thread thread = new Thread(testJoin);
        thread.start();


        for (int i = 0; i < 100; i++) {
            if (i == 20) {
                thread.join();
            }
            System.out.println("main线程  -----" + i);
        }

    }
}

线程状态观测

线程状态就是上面的所说的六个状态;

注意

  • 线程一旦死亡后,便无法再启动,对已死亡的线程调用start方法将会报错;

6、线程的优先级

  • Java提供一个线程调度器来监控程序中启动后进入就绪状态的所有线程,线程调度器根据线程的优先级来决定应该调度哪个线程来执行;
  • 线程优先级用数字表示,范围从1~10;
  • 使用以下方式改变或获取优先级(getPriority(), setPriority(int —));
package com.zeiyalo.state;

/**
 * @author Zeiyalo
 */
public class TestPriority implements Runnable {
    @Override
    public void run() {
        System.out.println(Thread.currentThread().getName() + "的优先级为 ---->" + Thread.currentThread().getPriority());
    }

    public static void main(String[] args) {
        TestPriority testPriority = new TestPriority();

        Thread t1 = new Thread(testPriority);
        Thread t2 = new Thread(testPriority);
        Thread t3 = new Thread(testPriority);

        /**
         * Thread-2的优先级为 ---->5
         * Thread-1的优先级为 ---->5
         * Thread-0的优先级为 ---->5
         *
         * 推测默认的优先级为5
         */
        t1.start();
        t2.start();
        t3.start();

        Thread t4 = new Thread(testPriority);
        Thread t5 = new Thread(testPriority);

        t4.setPriority(9);
        t5.setPriority(3);

        t5.start();
        t4.start();

        /**
         * 多次测试结果显示并非优先级高的一定优先执行,只是代表他被优先调度的概率大一些;
         */



    }

}

注意

  • 并非优先级高的一定优先执行,只是代表他被优先调度的概率大一些;

7、线程同步

不安全得三个线程问题:

不安全的买票

package com.zeiyalo.syn;

/**
 * @author Zeiyalo
 */
public class UnsafeBuyTicket {

    public static void main(String[] args) {
        BuyTicket buyTicket = new BuyTicket();

        new Thread(buyTicket, "老师").start();
        new Thread(buyTicket, "我").start();
        new Thread(buyTicket, "黄牛").start();
    }



}

class BuyTicket implements Runnable {

    private int ticketNum = 10;
    boolean flag = true;


    @Override
    public void run() {
        while (flag) {
            buy();
        }
    }

    private void buy() {
        if (ticketNum <= 0) {
            flag = false;
            return;
        }

        System.out.println(Thread.currentThread().getName() + "买到了第" + ticketNum-- +"张票");
    }

}

在这里插入图片描述

暂时并未出现不安全问题,但其实是不安全的;(黄牛抢票确实厉害)

银行取钱问题

package com.zeiyalo.syn;

/**
 * @author Zeiyalo
 */
public class UnsafeBank {
    public static void main(String[] args) {
        Account account = new Account(100, "家庭基金");

         new Drawing(50, account, "你").start();
         new Drawing(100, account, "老妈").start();

    }


}

class Account {
    int money;
    String name;

    public Account(int money, String name) {
        this.money = money;
        this.name = name;
    }
}

class Drawing extends Thread {
    private Account account;
    private int drawingMoney;
    private int nowMoney;

    public Drawing(int drawingMoney, Account account, String name) {
        super(name);
        this.drawingMoney = drawingMoney;
        this.account = account;
    }

    @Override
    public void run() {
        if (account.money - drawingMoney < 0) {
            System.out.println("钱不够,取不了");
            return;
        }

        // 放大问题的发生性
        try {
            Thread.sleep(1000);
        } catch (InterruptedException e) {
            e.printStackTrace();
        }

        account.money = account.money -drawingMoney;
        nowMoney = nowMoney + drawingMoney;

        System.out.println(account.name + "卡内余额为" + account.money);
        System.out.println(Thread.currentThread().getName() + "手里的钱" + nowMoney);
    }

}

测试结果:

在这里插入图片描述

集合的不安全性

package com.zeiyalo.syn;

import java.util.ArrayList;

/**
 * @author Zeiyalo
 */
public class UnsafeList {

    public static void main(String[] args) {
        ArrayList<String> list = new ArrayList<>();

        for (int i = 0; i < 10000; i++) {
            new Thread(() ->
                list.add(Thread.currentThread().getName())
            ).start();
        }

        System.out.println(list.size());
    }
}

测试结果:

在这里插入图片描述

测试结果到不了一万,因为多个线程添加同时覆盖了一些相同list的位置,所以导致了到不了一万,说明其实这个List中的add方法其实不是线程安全的;

解决上述问题,我们可以使用锁机制来解决上述问题;

synchronized关键字

使用 关键字可以实现对特定资源的加锁,首先介绍同步方法;

同步方法:

  • 同步方法可以用来锁定一个方法,当前线程运行该方法时,将会独占该锁,其他需要使用该方法的线程将堵塞,直到方法返回才会释放该锁,后面被阻塞的方法才能获得该锁,继续执行;
  • 弊端是一个大的复杂的方法时使用同步方法就会导致效率变的很慢,当一个方法中不仅有修改资源的操作,还有读取资源的操作时,读取资源的操作时间也被锁进去,很浪费资源;

上面同步方法的弊端显而易见,所以不是适用很多情况,这个时候就需要用到同步代码块;

同步代码块:

  • 同步代码块可以锁定对象或者属性,将对应的公共资源锁住后,在执行时就会查询该共享资源是否被锁住,如果被锁住,需要使用该共享资源的线程将被阻塞,否则将持锁运行;
  • 较为便利通过 synchronized (Object)可以较为灵活的去控制只在对应代码块锁住对应共享资源,不至于造成资源浪费;

买票案例(同步方法)

package com.zeiyalo.syn;

/**
 * @author Zeiyalo
 */
public class UnsafeBuyTicket {

    public static void main(String[] args) {
        BuyTicket buyTicket = new BuyTicket();

        new Thread(buyTicket, "老师").start();
        new Thread(buyTicket, "我").start();
        new Thread(buyTicket, "黄牛").start();
    }



}

class BuyTicket implements Runnable {

    private int ticketNum = 10;
    boolean flag = true;


    @Override
    public void run() {
        while (flag) {
            buy();
        }
    }

    private synchronized void buy() {
        if (ticketNum <= 0) {
            flag = false;
            return;
        }

        System.out.println(Thread.currentThread().getName() + "买到了第" + ticketNum-- +"张票");
    }

}

测试结果:

在这里插入图片描述

老师将所有的票买走,说明代码还有问题,当代码进入run方法时,持锁运行,因为flag未被置零,所以run中间的buy方法会反复进行,所以建议可以在run方法中加入sleep方法,这样在buy方法运行前或者运行后,当时当前线程还未持有锁或者已经释放方法锁资源,其他线程可以进入buy方法持锁运行;

注意:

  • sleep不能放在buy方法中,因为sleep不会释放锁资源,在方法中睡眠,锁资源还是不会释放,其他线程无法获取锁资源,也会被卡住,然后还是只能看着老师独自抢掉所有票;
  • sleep放在run方法中调用buy方法之前和之后的区别:
    • 之前:在之前调用sleep,第一个线程来了,直接先睡眠,然后其他线程进入,再次睡眠,第一个睡醒的线程拿到锁资源,运行之后,释放,CPU调度下一个循环睡眠起来的线程继续获取锁资源(这个是随机的,看CPU调度);
    • 之后:在之后调用sleep,第一个线程运行buy方法,其他线程被阻塞在buy方法之前请求CPU调度,然后第一个线程运行完,睡眠,因为第一个线程已经运行完一次buy方法,所以已经释放锁资源,这时按CPU调度给一个线程分配锁资源,然后循环运行;
    • 两者其实没啥区别,硬要说区别就是第一次进来的线程在第一种不一定第一个执行buy方法,第二个应该是第一个线程先执行buy方法;

修改后的代码

package com.zeiyalo.syn;

/**
 * @author Zeiyalo
 */
public class UnsafeBuyTicket {

    public static void main(String[] args) {
        BuyTicket buyTicket = new BuyTicket();

        new Thread(buyTicket, "老师").start();
        new Thread(buyTicket, "我").start();
        new Thread(buyTicket, "黄牛").start();
    }



}

class BuyTicket implements Runnable {

    private int ticketNum = 10;
    boolean flag = true;


    @Override
    public void run() {
        while (flag) {
            buy();

            try {
                Thread.sleep(100);
            } catch (InterruptedException e) {
                e.printStackTrace();
            }

        }
    }

    private synchronized void buy() {
        if (ticketNum <= 0) {
            flag = false;
            return;
        }

        System.out.println(Thread.currentThread().getName() + "买到了第" + ticketNum-- +"张票");
    }

}

测试结果:

在这里插入图片描述

由于测试代码为sleep在buy之后,根据之前分析,应该都为第一个线程第一个获取锁资源,测试多次,基本上都是老师第一个获取第十张票;

测试CopyOnWriteArrayList

CopyOnWriteArrayList 是 JUC 包中的一个线程安全的链表;

CopyOnWriteArrayList是一个线程安全的ArrayList,对其进行的修改操作都是在底层的一个复制的数组(快照)上进行的,也就是使用了写时复制策略。同时因为获取—修改—写入三步操作并不是原子性的,所以在增删改的过程中都使用了独占锁,来保证在某个时间只有一个线程能对list数组进行修改。

实现方法:

  • 每个CopyOnWriteArrayList对象里面有一个array数组对象用来存放具体元素;
  • ReentrantLock独占锁对象用来保证同时只有一个线程对array进行修改;

测试代码:

package com.zeiyalo.syn;

import java.util.concurrent.CopyOnWriteArrayList;

/**
 * @author Zeiyalo
 */
public class TestJUC {
    public static void main(String[] args) throws InterruptedException {
        CopyOnWriteArrayList<String> name = new CopyOnWriteArrayList<>();

        for (int i = 0; i < 10000; i++) {
            new Thread(() -> name.add(Thread.currentThread().getName())).start();
        }

        Thread.sleep(1000);

        System.out.println(name.size());
    }

    /**
     * 10000
     *
     * Process finished with exit code 0
     */
}

8、死锁

8.1、什么是死锁?

死锁,就是指两个或者两个以上的线程,在抢占两个或更多资源时,互相占有了对方所需要的资源,然后导致互相无限等待的情况;

8.2、死锁形成的条件?(四大条件)

死锁的形成有以下四大条件:

  • 互斥条件:一个资源每次只能被一个进程使用;
  • 请求与保持条件:一个进程会因为请求资源不到而被阻塞,对已获得的资源保持不放;
  • 不可强行占有条件:在一个资源被进程获取到之前,未使用完之前,不可被强行剥夺;
  • 循环等待条件:多个进程之间形成一种头尾相接的循环等待资源的关系;

8.3、死锁问题怎么避免?

对于死锁问题,可以通过破坏四个条件中的一个或者多个来进行解决;

  • 互斥条件:无法被破坏;
  • 请求与保持条件:可以一次性请求所有资源,并且在请求过程中先阻塞该进程,直到所有请求已就位,但该方法效率很低,不建议使用;
  • 不可抢占条件:
    • 可以限制当目前进程获取一个资源失败的时候,应释放之前按所持有的资源;
    • 或者如果当一个进程获取资源发现被另外一个进程占有时,可以请求操作系统抢占另一个进程,要求其释放已占有资源;
  • 循环等待条件:可以限定资源的获取顺序,每个进程都应该按照一定顺序去获取资源;

8.4、死锁的案例

化妆品抢占问题;

化妆需要口红和镜子,当两个人同时需要口红和镜子时,各自拿了一个然后等待另外一个释放;

package com.zeiyalo.syn;

/**
 * @author Zeiyalo
 */
public class DeadLock {

    public static void main(String[] args) {
        MakeUp mother = new MakeUp(0, "mother");
        MakeUp wife = new MakeUp(1, "wife");

        wife.start();
        mother.start();
    }
}

/**
 * 口红
 */
class LipStick {

}

/**
 * 镜子
 */
class Mirror {

}

class MakeUp extends Thread {
    static Mirror mirror = new Mirror();
    static LipStick lipStick = new LipStick();
    private int choice;

    public MakeUp(int choice, String name){
        super(name);
        this.choice = choice;
    }

    @Override
    public void run() {
        try {
            makeUp();
        } catch (InterruptedException e) {
            e.printStackTrace();
        }
    }

    public void makeUp() throws InterruptedException {
        if (choice == 0) {
            synchronized (mirror) {
                System.out.println(Thread.currentThread().getName() + "获得了镜子!");
                sleep(1000);
                synchronized (lipStick) {
                    System.out.println(Thread.currentThread().getName() + "获得了口红!");
                }
            }
        } else {
            synchronized (lipStick) {
                System.out.println(Thread.currentThread().getName() + "获得了口红!");
                sleep(1000);
                synchronized (mirror) {
                    System.out.println(Thread.currentThread().getName() + "获得了镜子!");
                }
            }
        }
    }

}

上面代码就会产生死锁问题,下面是一种修改方法:

package com.zeiyalo.syn;

/**
 * @author Zeiyalo
 */
public class DeadLock {

    public static void main(String[] args) {
        MakeUp mother = new MakeUp(0, "mother");
        MakeUp wife = new MakeUp(1, "wife");

        wife.start();
        mother.start();
    }
}

/**
 * 口红
 */
class LipStick {

}

/**
 * 镜子
 */
class Mirror {

}

class MakeUp extends Thread {
    static Mirror mirror = new Mirror();
    static LipStick lipStick = new LipStick();
    private int choice;

    public MakeUp(int choice, String name){
        super(name);
        this.choice = choice;
    }

    @Override
    public void run() {
        try {
            makeUp();
        } catch (InterruptedException e) {
            e.printStackTrace();
        }
    }

    public void makeUp() throws InterruptedException {
        if (choice == 0) {
            synchronized (mirror) {
                System.out.println(Thread.currentThread().getName() + "获得了镜子!");
                sleep(1000);

            }
            synchronized (lipStick) {
                System.out.println(Thread.currentThread().getName() + "获得了口红!");
            }
        } else {
            synchronized (lipStick) {
                System.out.println(Thread.currentThread().getName() + "获得了口红!");
                sleep(1000);

            }
            synchronized (mirror) {
                System.out.println(Thread.currentThread().getName() + "获得了镜子!");
            }
        }
    }

    /**
     * mother获得了镜子!
     * wife获得了口红!
     * mother获得了口红!
     * wife获得了镜子!
     *
     * Process finished with exit code 0
     */

}

还有其他的修改方式,大伙可以自己试试,或者以后有时间可以更上来,先不做过多赘述;

9、ReentrantLock(可重入锁)

在Java中定义了可重入锁,可以更方便的让我们锁住自己要锁住的部分代码;

package com.zeiyalo.syn;

/**
 * @author Zeiyalo
 */
public class TestLock {
    public static void main(String[] args) {
        BuyTicket2 buyTicket = new BuyTicket2();

        new Thread(buyTicket, "老师").start();
        new Thread(buyTicket, "我").start();
        new Thread(buyTicket, "黄牛").start();
    }
}

class BuyTicket2 implements Runnable {

    private int ticketNum = 10;


    @Override
    public void run() {
        while (true) {
            if (ticketNum > 0) {
                try {
                    Thread.sleep(1000);
                } catch (InterruptedException e) {
                    e.printStackTrace();
                }

                System.out.println(ticketNum--);
            } else {
                break;
            }
        }
    }

    /**
     * 9
     * 8
     * 10
     * 7
     * 6
     * 5
     * 4
     * 3
     * 4
     * 2
     * 0
     * 1
     *
     * Process finished with exit code 0
     */

}

明显可以看出出现了线程安全问题;

使用可重入锁:

package com.zeiyalo.syn;

import java.util.concurrent.locks.ReentrantLock;

/**
 * @author Zeiyalo
 */
public class TestLock {
    public static void main(String[] args) {
        BuyTicket2 buyTicket = new BuyTicket2();

        new Thread(buyTicket, "老师").start();
        new Thread(buyTicket, "我").start();
        new Thread(buyTicket, "黄牛").start();
    }
}

class BuyTicket2 implements Runnable {

    private int ticketNum = 10;
    /**
     * 初始化锁
     */
    private ReentrantLock lock = new ReentrantLock();


    @Override
    public void run() {
        while (true) {
            // 加锁
            lock.lock();
            try {
                // 执行代码块
                if (ticketNum > 0) {
                    try {
                        Thread.sleep(1000);
                    } catch (InterruptedException e) {
                        e.printStackTrace();
                    }

                    System.out.println(ticketNum--);
                } else {
                    break;
                }
            } catch (Exception e) {
                e.printStackTrace();
            } finally {
                // 解锁
                lock.unlock();
            }

        }
    }

}

/**
 * 10
 * 9
 * 8
 * 7
 * 6
 * 5
 * 4
 * 3
 * 2
 * 1
 *
 * Process finished with exit code 0
 */

下面就是可重入锁的常用格式:

			// 加锁
            lock.lock();
            try {
                // 执行代码块
                
                
                
                } else {
                    break;
                }
            } catch (Exception e) {
                e.printStackTrace();
            } finally {
                // 解锁
                lock.unlock();
            }

可重入锁和synchronized的区别:

  • ReentrantLock 是 JUC 包下的一个对象,而 synchronized 是Java的关键字;
  • 底层实现不同:
    • ReentrantLock 是通过CAS保证线程操作的原子性,和volatile关键字保证它的数据可见性来实现锁功能;
    • synchronized 的实现则是
  • ReentrantLock 可以对代码块进行加锁操作,而synchronized 不仅可以对代码块加锁还可以对方法加锁;
  • Lock锁使用时,JVM将花费较少的时间来调度线程,性能更好,而且它有更好的扩展性(提供更多子类)(jdk1.6之前,后来退出来了synchronized锁升级制度,两者性能就差不);
评论
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

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

抵扣说明:

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

余额充值