Java之多线程

一、创建线程

1.继承Thread类

  1. 自定义线程类继承Thread类
  2. 重写run()方法,编写线程执行体
  3. 创建线程对象,调用start()方法启动线程
//创建线程方式一:继承Thread类,重写run()方法,调用start开启线程
//注意:线程开启不一定立即执行,由cpu调度执行
public class TestThread1 extends Thread {
    @Override
    public void run() {
        //run方法线程体
        for (int i = 0; i < 1000; i++) {
            System.out.println("我在看代码---" + i);
        }
    }


    public static void main(String[] args) {
        //main线程,主线程

        //创建一个线程对象
        TestThread1 testThread1 = new TestThread1();

        //调用start()方法开启线程
        testThread1.start();

        for (int i = 0; i < 1000; i++) {
            System.out.println("我在学习多线程---" + i);
        }
    }
}

主线程中调用start()方法后,可以看到两个线程同时进行

image-20220116191712113

若将主线程的start()方法改为run()方法,则线程会顺序执行,先执行run()方法,再执行主线程

image-20220116192309532

start()方法和run()方法的区别

start()方法可启动线程,而run方法只是Thread类中的一个普通方法,还是在主线程中执行。

image-20220116192515918

练习 网图下载

image-20220116204141757

import org.apache.commons.io.FileUtils;

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

//练习Thread,实现多线程同步下载图片
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(url, name);
        System.out.println("下载了文件名为:" + name);
    }

    public static void main(String[] args) {
        TestThread2 t1 = new TestThread2("https://game.gtimg.cn/images/warframe/zlkdatasys/images//image/20200926/a89cd4baf5956ca18317038c6e78fb3d.png", "1.png");
        TestThread2 t2 = new TestThread2("https://game.gtimg.cn/images/warframe/zlkdatasys/images//image/20200926/7d2992ebc7c7c87392d4436ed0b6a12d.png", "2.png");
        TestThread2 t3 = new TestThread2("https://game.gtimg.cn/images/warframe/zlkdatasys/images//image/20200926/c713d25e1aadbd347c1efed07af6b398.png", "3.png");

        t1.start();
        t2.start();
        t3.start();
    }
}

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方法出现问题");
        }
    }
}

下载顺序不一致

image-20220116203611001

2.实现Runnable接口

  1. 定义MyRunnable类实现Runnable接口
  2. 实现run()方法,编写线程执行体
  3. 创建线程对象,调用start()方法启动线程
//创建线程方式2:实现Runnable接口,重写run方法,执行线程需要丢入Runnable接口实现类,调用start方法
public class TestThread3 implements Runnable {
    
    @Override
    public void run() {
        //run方法线程体
        for (int i = 0; i < 1000; i++) {
            System.out.println("我在看代码---" + i);
        }
    }


    public static void main(String[] args) {
        //创建Runnable接口的实现类对象
        TestThread3 testThread3 = new TestThread3();

        //创建线程对象,通过线程对象来开启我们的线程,代理
//        Thread thread=new Thread(testThread3);
//        thread.start();

        new Thread(testThread3).start();


        for (int i = 0; i < 1000; i++) {
            System.out.println("我在学习多线程---" + i);
        }
    }

}

调用了start()方法,测试线程和主线程同时进行

image-20220120160901186

练习 龟兔赛跑

  1. 首先来个赛道距离,然后要离终点越来越近
  2. 判断比赛是否结束
  3. 打印出胜利者
  4. 故事中是乌龟赢的,兔子需要睡觉,用sleep()模拟兔子睡觉
public class Race implements Runnable {

    //胜利者
    private static String winner;

    @Override
    public void run() {
        for (int i = 0; i <= 100; i++) {

            //模拟兔子休息,每跑20步休息1毫秒
            if (Thread.currentThread().getName().equals("兔子") && i % 20 == 0) {
                try {
                    Thread.sleep(1);
                } catch (InterruptedException e) {
                    e.printStackTrace();
                }
            }

            //判断比赛是否结束
            boolean flag = gameOver(i);
            //如果比赛结束了,就停止程序
            if (flag) {
                break;
            }

            System.out.println(Thread.currentThread().getName() + "-->跑了" + i + "步");
        }
    }


    //判断是否完成比赛
    private boolean gameOver(int steps) {
        //判断是否有胜利者
        if (winner != null) {//已经存在胜利者了
            return true;
        } else if (steps >= 100) {
            winner = Thread.currentThread().getName();
            System.out.println("winner is" + winner);
            return true;
        } else {
            return false;
        }

    }

    public static void main(String[] args) {
        Race race = new Race();

        new Thread(race, "兔子").start();
        new Thread(race, "乌龟").start();
    }
}

运行结果:由于cpu的高速运作,乌龟总是赢

image-20220124170738976

继承Thread类和实现Runnable接口比较

继承Thread类

  • 子类继承Thread类具备多线程能力
  • 启动线程:子类对象.start()
  • 不建议使用:避免OOP单继承局限性

实现Runnable接口

  • 实现Runnable接口具有多线程能力
  • 启动线程:传入目标对象,Thread对象.start()
  • 推荐使用:避免单继承局限性,灵活方便,方便同一个对象被多个线程使用

3.实现Callable接口

  1. 实现Callable接口,需要返回值类型
  2. 重写call方法,需要抛出异常
  3. 创建目标对象
  4. 创建执行服务:ExecutorService ser = Executors.newFixedThreadPool(1);
  5. 提交执行:Future result1 = ser.submit(t1);
  6. 获取结果:boolean rs1 = result1.get();
  7. 关闭服务:ser.shutdown();
import java.util.concurrent.*;

public class TestCallable implements Callable<Boolean> {
    private String name;

    public TestCallable(String name) {
        this.name = name;
    }

    @Override
    public Boolean call() {
        //call方法线程体
        for (int i = 0; i < 1000; i++) {
            System.out.println(name + "--->" + i);
        }
        return true;
    }


    public static void main(String[] args) throws ExecutionException, InterruptedException {
        //创建Callable接口的实现类对象
        TestCallable t1 = new TestCallable("线程1");
        TestCallable t2 = new TestCallable("线程2");


        //创建执行服务
        ExecutorService ser = Executors.newFixedThreadPool(2);

        //提交执行
        Future<Boolean> r1 = ser.submit(t1);
        Future<Boolean> r2 = ser.submit(t2);

        //获取结果
        boolean rs1 = r1.get();
        boolean rs2 = r2.get();

        //关闭结果
        ser.shutdown();


    }
}

可以看到两个线程同时执行

image-20220124175706369

二、Lambda表达式

简述

为什么要使用Lambda表达式

  • 避免匿名内部类定义过多

  • 可以让你的代码看起来很简介

  • 去掉了一堆没有意义的代码,只留下核心的逻辑

  • 其实质属于函数式编程的概念

语法格式为

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

实现

Lambda表达式可以对函数式接口进行简单的实现,函数式接口(Functional Interface)就是一个有且仅有一个抽象方法,但是可以有多个非抽象方法的接口。

先通过各种内部类和Lambda表达式的比较来感受Lambda表达式的简洁,这是一个无参无返回值的方法

//推导lambda表达式
public class TestLambda {

    public static void main(String[] args) {
        //1.最原始的方法
        ILike like = new Like();
        like.lambda();

        //2.静态内部类Like2
        like = new Like2();
        like.lambda();

        //3.局部内部类Like3
        class Like3 implements ILike {
            @Override
            public void lambda() {
                System.out.println("i like lambda3");
            }
        }
        like = new Like3();
        like.lambda();

        //4.匿名内部类,没有类的名称,必须借助接口或者父类
        like = new ILike() {
            @Override
            public void lambda() {
                System.out.println("i like lambda4");
            }
        };
        like.lambda();

        //5.用lambda简化
        like = () -> {
            System.out.println("i like lambda5");
        };
        like.lambda();

    }
    
    //静态内部类Like2
    static class Like2 implements ILike {
        @Override
        public void lambda() {
            System.out.println("i like lambda2");
        }
    }

}


//定义一个函数式接口
interface ILike {
    void lambda();
}

//实现类
class Like implements ILike {
    @Override
    public void lambda() {
        System.out.println("i like lambda");
    }
}

运行结果

i like lambda
i like lambda2
i like lambda3
i like lambda4
i like lambda5

再来看一个简单的例子,这是一个有参有返回值的方法

public class TestLambda3 {

    public static void main(String[] args) {
        //Lambda表达式
        Test test=(int a,int b)->{
            return a+b;
        };
        
        int res= test.add(5,3);
        System.out.println(test.add(5,3));
    }
}

//函数式接口
interface Test{
    public int add(int a,int b);
    }
}

运行结果

8

如果函数式接口中有非抽象方法,不会对Lambda表达式产生影响,Lambda表达式只对接口中的那一个抽象方法进行实现

public class TestLambda3 {

    public static void main(String[] args) {
        //Lambda表达式实现add()抽象方法
        Test test=(int a,int b)->{
            return a+b;
        };
        
        int res= test.add(5,3);
        System.out.println(test.add(5,3));
        System.out.println(test.subtract(5,3));
    }
}

interface Test{
    //抽象方法
    public int add(int a,int b);
    //非抽象方法
    public default int subtract(int a,int b){
        return a-b;
    }
}

运行结果

8
2

简化

Lambda表达式还可进一步简化

public class TestLambda3 {

    public static void main(String[] args) {
        Test test=(int a,int b)->{
            return a+b;
        };
        System.out.println(test.add(5,3));

        //1.Lambda表达式可以不写参数类型,但必须都不写
        //像test=(int a,b)->... 参数这样写是不行的
        test=(a,b)->{
            return a+b;
        };
        System.out.println(test.add(5,3));

        //2.如果只有一个参数,包裹参数的小括号可以不写
        Test2 test2=a->{
            System.out.println("a = "+a);
        };
        test2.print(2);

        //3.如果方法体中只有一行参数,则大括号可以不写
        test2=a->System.out.println("a = "+a);
        test2.print(3);

    }
}

interface Test{
    public int add(int a,int b);
}

interface Test2{
    public void print(int a);
}

运行结果

8
8
a = 2
a = 3

三、线程状态

image-20220205125738855

线程停止

  • 不推荐使用JDK提供的stop()、destroy()方法。【已废弃】
  • 推荐线程自己停止下来
  • 建议使用一个标志位进行终止,当flag=false,则终止线程运行
//设置一个标志位测试线程停止
public class TestStop implements Runnable{
    
	//标志位
    private  boolean flag=true;

    @Override
    public void run() {
        int i=0;
        while(flag){
            System.out.println("run...Thread"+i++);
        }
    }

    public void stop(){
        this.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("run...main"+i);
            if(i==900){
                //调用stop方法切换标志位,让线程停止
                testStop.stop();
                System.out.println("Thread停止");
            }
        }
    }
}

运行结果

run...main0
run...main1
run...main2
...
run...Thread0
run...main52
run...Thread1
run...Thread2
run...Thread3
run...main53
...
run...main896
run...main897
run...main898
run...main899
run...main900
Thread停止
run...main901
run...main902
...
run...main999

线程休眠 sleep

  • sleep指定当前线程阻塞的毫秒数
  • sleep存在异常InterruptedException
  • sleep时间达到后线程进入就绪状态
  • sleep可以模拟网络延时,倒计时等
  • 每一个对象都有一个锁,sleep不会释放锁

模拟倒计时

public class TestSleep {

    public static void main(String[] args) {
        try {
            tenDown();
        } catch (InterruptedException e) {
            e.printStackTrace();
        }
    }

    //模拟倒计时
    public static void tenDown() throws InterruptedException {
        int num = 10;
        while (true) {
            Thread.sleep(1000);
            System.out.println(num--);
            if (num <= 0) break;
        }
    }
}

运行结果

10
9
8
7
6
5
4
3
2
1

打印系统当前时间

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

public class TestSleep {

    public static void main(String[] args) {
        Date startTime = new Date(System.currentTimeMillis());//获取系统当前时间

        while (true) {
            try {
                Thread.sleep(1000);
                System.out.println(new SimpleDateFormat("HH:mm:ss").format(startTime));
                startTime = new Date(System.currentTimeMillis());//更新当前时间
            } catch (InterruptedException e) {
                e.printStackTrace();
            }
        }
    }

}

运行结果

20:44:51
20:44:52
20:44:53
20:44:54
20:44:55
20:44:56
...

线程礼让 yield

  • 礼让线程,让当前正在执行的线程暂停,但不阻塞

  • 将线程从运行态转为就绪态

  • 让cpu重新调度,礼让不一定成功!看cpu心情

打个比方,有两个线程a和b,cpu在执行a,a做出了礼让,此时cpu会重新选择a或者b来执行,有可能又选择了a执行(礼让失败),有可能选择了b执行(礼让成功)

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

        Thread_A thread_a = new Thread_A();
        new Thread(thread_a, "a").start();

        Thread_B thread_b = new Thread_B();
        new Thread(thread_b, "b").start();

    }

}

class Thread_A implements Runnable {
    @Override
    public void run() {
        System.out.println(Thread.currentThread().getName() + "线程开始执行");
        //a线程礼让
        Thread.yield();
        System.out.println(Thread.currentThread().getName() + "线程停止执行");
    }
}

class Thread_B implements Runnable {
    @Override
    public void run() {
        System.out.println(Thread.currentThread().getName() + "线程开始执行");
        System.out.println(Thread.currentThread().getName() + "线程停止执行");
    }
}

运行结果

a线程开始执行
b线程开始执行
b线程停止执行
a线程停止执行

线程a中加了yield()只是增大了出现以上运行结果的概率,如果不加yield(),同样会出现以上运行结果,只是概率比较低而已。

线程强制执行 join

  • join合并线程,待此线程执行完成后,再执行其他前程,其他线程阻塞
  • 可以想象成插队
package ThreadState;

public class TestJoin implements Runnable {
    @Override
    public void run() {
        for (int i = 0; i < 100; i++) {
            System.out.println("线程vip来了" + i);
        }
    }

    public static void main(String[] args) throws InterruptedException {

        TestJoin testJoin = new TestJoin();
        Thread thread = new Thread(testJoin);
		
        //主线程
        for (int i = 0; i < 500; i++) {
            if (i == 200) {
                //其他线程
                thread.start();
                //其他线程执行join()
                thread.join();
            }
            System.out.println("main" + i);
        }

    }
    
}

运行结果

main0
main1
...
main197
main198
main199
线程vip来了0
线程vip来了1
线程vip来了2
...

观测线程状态

img

线程可以处于一下状态之一:

  • NEW 尚未启动的线程
  • RUNNABLE 在Java虚拟机中执行的线程
  • BLOCKED 被阻塞等待监视器锁定的线程
  • WAITING 正在等待另一个线程执行特定动作的线程
  • TIMED_WAITING 正在等待另一个线程执行动作达到指定等待时间的线程
  • TERMINATED 已退出的线程

Thread.State是一个枚举类型

image-20220128175120739

//观察测试线程的状态
public class TestState {

    public static void main(String[] args) throws InterruptedException {
        Thread thread = new Thread(() -> {
            for (int i = 0; i < 5; i++) {
                try {
                    Thread.sleep(1000);
                } catch (InterruptedException e) {
                    e.printStackTrace();
                }
            }
            System.out.println("////////////////////");
        });

        //观察状态
        Thread.State state = thread.getState();
        System.out.println(state);  //NEW

        //观察启动后
        thread.start();
        state = thread.getState();
        System.out.println(state);  //RUNNABLE

        while (state != Thread.State.TERMINATED) {   //只要线程不终止,就一直输出状态
            Thread.sleep(100);
            state = thread.getState();  //更新线程状态
            System.out.println(state);
        }

    }
}

运行结果

NEW
RUNNABLE
TIMED_WAITING
TIMED_WAITING
TIMED_WAITING
...
TIMED_WAITING
TIMED_WAITING
TIMED_WAITING
////////////////////
TERMINATED

线程优先级

  • Java提供一个线程调度器来监控程序中启动后进入就绪状态的所有线程,线程调度器按照优先级决定应该调度哪个线程来执行

  • 线程的优先级用数字表示,范围从0~10

  • 使用getPriority()和setPriority()来获取和改变优先级

  • 优先级的设定建议在start()调度前

    默认优先级为5

    image-20220129142620744

    Thread.setPriority()

    image-20220129142744479

public class TestPriority {
    public static void main(String[] args) {
        MyPriority myPriority = new MyPriority();
        Thread t0 = new Thread(myPriority);
        Thread t1 = new Thread(myPriority);
        Thread t2 = new Thread(myPriority);
        Thread t3 = new Thread(myPriority);
        Thread t4 = new Thread(myPriority);
        Thread t5 = new Thread(myPriority);

        t0.start();
		
        t1.setPriority(1);
        t1.start();

        t2.setPriority(4);
        t2.start();

        t3.setPriority(10);
        t3.start();

        t4.setPriority(8);
        t4.start();

        t5.setPriority(7);
        t5.start();
    }
}

class MyPriority implements Runnable {
    @Override
    public void run() {
        System.out.println(Thread.currentThread().getName() + "-->" + Thread.currentThread().getPriority());
    }
}

运行结果

Thread-3-->10
Thread-4-->8
Thread-5-->7
Thread-0-->5
Thread-2-->4
Thread-1-->1

事实上,优先级低只是意味着获得调度的概率低,并不是优先级低就不会被调用了,这取决于cpu的线程调度算法。

还会出现各种运行结果,如

Thread-0-->5
Thread-3-->10
Thread-4-->8
Thread-5-->7
Thread-2-->4
Thread-1-->1

守护线程

  • 线程分为用户线程和守护线程
  • 虚拟机必须确保用户线程执行完毕
  • 虚拟机不用等待守护线程执行完毕
  • 如:后台记录操作日志,监控内存,垃圾回收等

使用setDaemon()方法设置守护线程

//测试守护线程
//上帝守护你
public class TestDaemon {
    public static void main(String[] args) {
        God god = new God();
        You you = new You();

        Thread thread = new Thread(god);
        thread.setDaemon(true);  //默认是false表示是用户线程,正常的线程都是用户线程

        thread.start();  //上帝守护线程启动

        new Thread(you).start();  //你 用户线程启动
    }
}


//上帝
class God implements Runnable {
    @Override
    public void run() {
        while (true) {
            System.out.println("上帝保佑着你");
        }
    }
}

//你
class You implements Runnable {
    @Override
    public void run() {
        for (int i = 0; i < 36500; i++) {
            System.out.println("你一生都开心地活着");
        }
        System.out.println("===Goodbye,World!===");
    }
}

运行结果:主线程终止后,守护线程也终止了。

...
上帝保佑着你
上帝保佑着你
上帝保佑着你
你一生都开心地活着
你一生都开心地活着
你一生都开心地活着
你一生都开心地活着
你一生都开心地活着
你一生都开心地活着
===Goodbye,World!===
上帝保佑着你
上帝保佑着你
上帝保佑着你
上帝保佑着你

Process finished with exit code 0

四、并发

并发就是同一个对象被多个线程同时操作,如果不对并发加以控制,会引起有关线程同步和互斥的问题。

一个不安全的例子:

//多个线程同时操作同一个对象
//买火车票的例子

//发现问题:多个线程操作同一个资源的情况下,线程不安全,数据紊乱
public class TestThread4 implements Runnable {

    //票数
    private int ticketNums = 10;
    private boolean flag = true;

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

    public void buy() {
        if (ticketNums <= 0) {
            flag = false;
            return;
        }
        //模拟延时
        try {
            Thread.sleep(200);
        } catch (InterruptedException e) {
            e.printStackTrace();
        }
        System.out.println(Thread.currentThread().getName() + "-->拿到了第" + ticketNums-- + "张票");
    }


    public static void main(String[] args) {
        TestThread4 ticket = new TestThread4();

        new Thread(ticket, "小明").start();
        new Thread(ticket, "老师").start();
        new Thread(ticket, "黄牛党").start();
    }
}

运行结果:可以看到小明和黄牛党都拿到了"第7张票"、出现了"第0张票、第-1张票"等数据紊乱的问题

黄牛党-->拿到了第10张票
小明-->拿到了第9张票
老师-->拿到了第8张票
小明-->拿到了第7张票
老师-->拿到了第6张票
黄牛党-->拿到了第7张票
老师-->拿到了第5张票
黄牛党-->拿到了第4张票
小明-->拿到了第3张票
老师-->拿到了第2张票
黄牛党-->拿到了第1张票
小明-->拿到了第0张票
老师-->拿到了第-1张票

线程同步

由于同一进程的多个线程共享同一块存储空间,在带来方便的同时,也带来了访问冲突问题,为了保证数据在方法中被访问时的正确性,在访问时加入锁机制synchronized,当一个线程获得对象的排它锁,独占资源,其他线程必须等待,使用后释放锁即可。

存在以下问题:

  • 一个线程持有锁会导致其他所有需要此锁的线程挂起
  • 在多个线程竞争下,加锁、释放锁会导致比较多的上下文切换和调度延时,引起性能问题
  • 如果一个优先级高的线程等待一个优先级低的线程释放锁会导致优先级倒置,引起性能问题

synchronized

synchronized共有三种用法

  1. 修饰普通方法 对实例对象加了锁,对实例对象中synchronized修饰的方法的访问需要获得对象的锁
  2. 修饰静态方法 对类加了锁,对类中synchronized修饰的方法进行访问需要获得这个类的锁
  3. 修饰代码块 synchronized(obj){ },obj可以是任何对象,对obj加了锁,对代码块的访问需要获得obj的锁。

修饰普通方法

//上述例子
public class TestThread4 implements Runnable {

    //票数
    private int ticketNums = 10;
    private boolean flag = true;

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

    //synchornized修饰普通方法
    public synchronized void buy() {
        if (ticketNums <= 0) {
            flag = false;
            return;
        }
        //模拟延时
        try {
            Thread.sleep(200);
        } catch (InterruptedException e) {
            e.printStackTrace();
        }
        System.out.println(Thread.currentThread().getName() + "-->拿到了第" + ticketNums-- + "张票");
    }


    public static void main(String[] args) {
        TestThread4 ticket = new TestThread4();

        new Thread(ticket, "小明").start();
        new Thread(ticket, "老师").start();
        new Thread(ticket, "黄牛党").start();
    }
}

运行结果:对buy()方法加了synchronized后,锁定了ticket对象,当一个线程进入buy()方法前,要获得ticket对象的锁,因此不会出现买到了同一张票、买到了第-1张票等问题。

小明-->拿到了第10张票
黄牛党-->拿到了第9张票
老师-->拿到了第8张票
黄牛党-->拿到了第7张票
小明-->拿到了第6张票
黄牛党-->拿到了第5张票
老师-->拿到了第4张票
老师-->拿到了第3张票
黄牛党-->拿到了第2张票
黄牛党-->拿到了第1张票

修饰静态方法

对上述买票情景稍作修改:现在有两个售票商ticket1和ticket2,共同售卖10张票,小明和老师两个人去抢票。

出现了新的问题:

public class TestThread4 implements Runnable {

    //静态类型,共享
    private static int ticketNums = 10;
    private static boolean flag = true;

    @Override
    public void run() {
        while (flag) {
            buy();
        }
    }
    
	//synchronized修饰普通方法
    public synchronized void buy() {
        if (ticketNums <= 0) {
            flag = false;
            return;
        }
        //模拟延时
        try {
            Thread.sleep(200);
        } catch (InterruptedException e) {
            e.printStackTrace();
        }
        System.out.println(Thread.currentThread().getName() + "-->拿到了第" + ticketNums-- + "张票");
    }


    public static void main(String[] args) {
        TestThread4 ticket1 = new TestThread4();
        TestThread4 ticket2 = new TestThread4();

        new Thread(ticket1, "小明").start();
        new Thread(ticket2, "老师").start();
    }
}

运行结果:虽然对buy()加了synchronized,但是仍然没有实现对票数的互斥访问,老师买到了第0张票。这是因为,synchronized修饰普通方法是对实例对象加了锁,现在有ticket1和ticket2两个对象,那么就有两把锁。两把锁维护一个临界资源ticketNums是起不到互斥访问的目的的。

老师-->拿到了第10张票
小明-->拿到了第9张票
小明-->拿到了第8张票
老师-->拿到了第7张票
小明-->拿到了第6张票
老师-->拿到了第5张票
老师-->拿到了第4张票
小明-->拿到了第3张票
老师-->拿到了第2张票
小明-->拿到了第1张票
老师-->拿到了第0张票

解决办法是用synchronized修饰静态方法buy()

public class TestThread4 implements Runnable {

    private static int ticketNums = 10;
    private static boolean flag = true;

    @Override
    public void run() {
        while (flag) {
            buy();
        }
    }
	
    //synchronized修饰静态方法
    public synchronized static void buy() {
        if (ticketNums <= 0) {
            flag = false;
            return;
        }
        //模拟延时
        try {
            Thread.sleep(200);
        } catch (InterruptedException e) {
            e.printStackTrace();
        }
        System.out.println(Thread.currentThread().getName() + "-->拿到了第" + ticketNums-- + "张票");
    }


    public static void main(String[] args) {
        TestThread4 ticket1 = new TestThread4();
        TestThread4 ticket2 = new TestThread4();

        new Thread(ticket1, "小明").start();
        new Thread(ticket2, "老师").start();
    }
}

运行结果:没有出现问题,这是因为,synchronized修饰静态方法,是对类加了锁,无论哪个对象的哪个线程进入buy()方法前都需要先获得这个类的锁。

小明-->拿到了第10张票
小明-->拿到了第9张票
老师-->拿到了第8张票
小明-->拿到了第7张票
老师-->拿到了第6张票
小明-->拿到了第5张票
老师-->拿到了第4张票
老师-->拿到了第3张票
小明-->拿到了第2张票
老师-->拿到了第1张票

修饰代码块

需要指定加锁的对象,当线程进入代码块前,要获得指定对象的锁

public class TestThread4 implements Runnable {

    //票数
    private int ticketNums = 10;
    private boolean flag = true;

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


    public void buy() {
        //synchronized修饰代码块
        synchronized (this) {
            if (ticketNums <= 0) {
                flag = false;
                return;
            }
            //模拟延时
            try {
                Thread.sleep(200);
            } catch (InterruptedException e) {
                e.printStackTrace();
            }
            System.out.println(Thread.currentThread().getName() + "-->拿到了第" + ticketNums-- + "张票");
        }
    }


    public static void main(String[] args) {
        TestThread4 ticket = new TestThread4();

        new Thread(ticket, "小明").start();
        new Thread(ticket, "老师").start();
        new Thread(ticket, "黄牛党").start();
    }
}

运行结果:没有出现问题,对代码块加了synchronized并且指定为ticket对象(this),对代码块的访问就需要先获得ticket对象的锁。

小明-->拿到了第10张票
老师-->拿到了第9张票
老师-->拿到了第8张票
黄牛党-->拿到了第7张票
老师-->拿到了第6张票
小明-->拿到了第5张票
老师-->拿到了第4张票
老师-->拿到了第3张票
黄牛党-->拿到了第2张票
老师-->拿到了第1张票

Lock

  • 从JDK5.0开始,Java提供了更强大的线程同步机制——通过显式定义同步锁对象来实现同步。同步锁使用Lock对象充当

  • java.util.concurrent.locks.Lock接口是控制多个线程对共享资源进行访问的工具。锁提供了对共享资源的独占访问,每次只能有一个线程对Lock对象加锁,线程开始访问共享资源之前应先获得Lock对象

  • ReentrantLock类实现了Lock,它拥有与synchronized相同的并发性和内存语义,在实现线程安全的控制中,比较常用的是ReentrantLock,可以显式加锁、释放锁。

一般使用格式是

Private final ReentrantLock lock = new ReentrantLock();
lock.lock();
try {
    //保证线程安全的代码
} finally {
    lock.unlock();
}

例子

import java.util.concurrent.locks.ReentrantLock;

//测试Lock锁
public class TestLock {
    public static void main(String[] args) {

        TestLock2 testLock2 = new TestLock2();

        new Thread(testLock2).start();
        new Thread(testLock2).start();
        new Thread(testLock2).start();
    }
}

class TestLock2 implements Runnable {

    int ticketNums = 10;

    //定义lock锁
    private final ReentrantLock lock = new ReentrantLock();

    @Override
    public void run() {
        while (true) {
            try {
                //加锁
                lock.lock();
                if (ticketNums > 0) {
                    try {
                        Thread.sleep(1000);
                    } catch (InterruptedException e) {
                        e.printStackTrace();
                    }
                    System.out.println(ticketNums--);
                } else {
                    break;
                }
            } finally {
                //解锁
                lock.unlock();
            }
        }
    }
}

运行结果:三个线程互斥访问ticketNums临界资源

10
9
8
7
6
5
4
3
2
1

Process finished with exit code 0

synchronized和Lock比较

  • Lock是显式锁(手动开启和关闭锁,别忘记关闭锁),synchronized是隐式锁,出了作用域自动释放
  • Lock只有代码块锁,synchronized有代码块锁和方法锁
  • 使用Lock锁,JVM将花费较少的时间来调度线程,性能更好。并且具有更好的扩展性(提供更多的子类)
  • 优先使用顺序:Lock > 同步代码块 > 同步方法

死锁

多个线程各自占有一些共享资源,并且互相等待其他线程占有的资源释放才能运行,而导致两个或者多个线程都在等待对方释放资源,都停止执行的情景。某一个同步块同时拥有“两个以上对象的锁时”,就可能发生“死锁”的问题。

public class DeadLock {
    public static void main(String[] args) {
        Makeup g1 = new Makeup(0, "g1");
        Makeup g2 = new Makeup(1, "g2");

        g1.start();
        g2.start();
    }
}

//口红
class Lipstick {

}

//镜子
class Mirror {

}

class Makeup extends Thread {

    //需要的资源只有一份,用static
    static Lipstick lipstick = new Lipstick();
    static Mirror mirror = new Mirror();

    int choice;  //选择方案
    String girlName;  //使用化妆品的人

    Makeup(int choice, String girlName) {
        this.choice = choice;
        this.girlName = girlName;
    }

    @Override
    public void run() {
        //化妆
        try {
            makeup();
        } catch (InterruptedException e) {
            e.printStackTrace();
        }
    }

    //化妆,互相持有对方的锁
    private void makeup() throws InterruptedException {
        if (choice == 0) {
            synchronized (lipstick) {
                System.out.println(this.girlName + "获得口红的锁");
                Thread.sleep(1000);  //一秒钟后想要拿起镜子
                synchronized (mirror) {
                    System.out.println(this.girlName + "获得镜子的锁");
                }
            }
        } else {
            synchronized (mirror) {
                System.out.println(this.girlName + "获得镜子的锁");
                Thread.sleep(1000);  //一秒钟后想要拿起口红
                synchronized (lipstick) {
                    System.out.println(this.girlName + "获得口红的锁");
                }
            }
        }
    }
}

运行结果:g1要获得g2拥有的锁才能运行,g2要获得g1拥有的锁才能运行,发生死锁

g1获得口红的锁
g2获得镜子的锁

Process finished with exit code -1

死锁避免方法

产生死锁的四个必要条件:

  1. 互斥条件:一个资源每次只能被一个进程使用
  2. 请求与保持条件:一个进程因请求资源而阻塞时,对已获得的资源保持不放
  3. 不可剥夺条件:进程已获得的资源,在未使用完之前,不能强行剥夺
  4. 循环等待条件:若干进程之间形成一种头尾相接的循环等待资源关系

想办法破坏其中任意一个或多个条件就可以避免死锁发生

生产者消费者问题

生产者消费者问题(Producer-consumer problem),也称有限缓冲问题(Bounded-buffer problem),是一个多线程同步问题的经典案例。该问题描述了两个共享固定大小缓冲区的线程——即所谓的“生产者”和“消费者”——在实际运行时会发生的问题。

生产者的主要作用是生成一定量的数据放到缓冲区中,然后重复此过程。与此同时,消费者也在缓冲区消耗这些数据。该问题的关键就是要保证生产者不会在缓冲区满时加入数据,消费者也不会在缓冲区中空时消耗数据。

Java提供了几个方法解决线程之间的通信问题

  • wait() 表示线程一直等待,直到其他线程通知,与sleep不同,会释放锁
  • wait(long timeout) 指定等待的毫秒数
  • notify() 唤醒一个处于等待状态的线程
  • notifyAll() 唤醒同一个对象上所有调用wait()方法的线程,优先级别高的线程优先调度

注意:这些都是Object类的方法,都只能在同步方法或者同步代码块中使用,否则会抛出异常IllegalmonitorstateException

解决生产者消费者问题的办法有管程法、信号灯法等

管程法

  • 生产者:负责生产数据的模块(可能是方法,对象,线程,进程)
  • 消费者:负责处理数据的模块(可能是方法,对象,线程,进程)
  • 缓冲区:消费者不能直接使用生产者的数据,他们之间有个“缓冲区

生产者将生产好的数据放入缓冲区,消费者从缓冲区拿出数据

//测试:生产者消费者模型-->利用缓冲区解决:管程法
public class TestPC {

    public static void main(String[] args) {
        SynContainer container = new SynContainer();

        new Producer(container).start();
        new Consumer(container).start();
    }
}

//生产者
class Producer extends Thread {
    SynContainer container;

    public Producer(SynContainer container) {
        this.container = container;
    }

    //生产
    @Override
    public void run() {
        for (int i = 1; i <= 100; i++) {
            container.push(new Chicken(i));
        }
    }
}

//消费者
class Consumer extends Thread {
    SynContainer container;

    public Consumer(SynContainer container) {
        this.container = container;
    }

    //消费
    @Override
    public void run() {
        for (int i = 1; i <= 100; i++) {
            container.pop();

        }
    }
}

//产品
class Chicken {
    int id;

    public Chicken(int id) {
        this.id = id;
    }
}

//缓冲区
class SynContainer {

    //需要一个容器大小
    Chicken[] chickens = new Chicken[10];

    //容器计数器
    int count = 0;


    //生产者放入产品
    public synchronized void push(Chicken chicken) {
        //如果容器满了,就需要等待消费者消费
        while (count == chickens.length) {
            //等待消费者消费,生存者等待
            try {
                this.wait();
            } catch (InterruptedException e) {
                e.printStackTrace();
            }

        }

        //如果没满,就需要丢入产品
        chickens[count] = chicken;
        count++;
        System.out.println("生产了第" + chicken.id + "只鸡");

        //通知消费者可以消费了
        this.notifyAll();
    }

    //消费者消费产品
    public synchronized void pop() {
        //判断能否消费
        while (count == 0) {
            //等待生产者生产,消费者等待
            try {
                this.wait();
            } catch (InterruptedException e) {
                e.printStackTrace();
            }
        }

        //如果可以消费
        count--;
        System.out.println("消费了第" + chickens[count].id + "只鸡");

        //吃完了,通知生产者生产
        this.notifyAll();
    }
}

运行结果

生产了第1只鸡
生产了第2只鸡
消费了第2只鸡
消费了第1只鸡
生产了第3只鸡
消费了第3只鸡
生产了第4只鸡
消费了第4只鸡
生产了第5只鸡
消费了第5只鸡
生产了第6只鸡
消费了第6只鸡
生产了第7只鸡
消费了第7只鸡
生产了第8只鸡
消费了第8只鸡
生产了第9只鸡
消费了第9只鸡
生产了第10只鸡
消费了第10只鸡
...
生产了第96只鸡
生产了第97只鸡
生产了第98只鸡
生产了第99只鸡
生产了第100只鸡
消费了第100只鸡
消费了第99只鸡
消费了第98只鸡
消费了第97只鸡
消费了第96只鸡

Process finished with exit code 0

信号灯法

设置一个flag标志,为true则生产者生存,消费者等待;为false则消费者消费,生产者等待

类似于设置了一个大小为1的缓冲区

//测试生产者消费问题2:信号灯法,标志位解决
public class TestPC2 {
    public static void main(String[] args) {
        Food food = new Food();
        new Mom(food).start();
        new Son(food).start();
    }
}

//生产者-->妈妈
class Mom extends Thread {
    Food food;

    public Mom(Food food) {
        this.food = food;
    }

    @Override
    public void run() {
        for (int i = 0; i < 20; i++) {
            if (i % 2 == 0) {
                this.food.produce("米饭");
            } else {
                this.food.produce("面条");
            }
        }
    }
}

//消费者-->儿子
class Son extends Thread {
    Food food;

    public Son(Food food) {
        this.food = food;
    }

    @Override
    public void run() {
        for (int i = 0; i < 20; i++) {
            food.consume();
        }
    }
}

//产品-->食物
class Food {
    //生产的食物
    String food;
    //妈妈做饭,儿子等待  True
    //儿子吃饭,妈妈等待  False
    boolean flag = true;

    //妈妈做饭
    public synchronized void produce(String food) {
        if (!flag) {
            try {
                this.wait();
            } catch (InterruptedException e) {
                e.printStackTrace();
            }
        }
        System.out.println("妈妈做了:" + food);
        //通知儿子吃
        this.notifyAll();
        this.food = food;
        this.flag = !flag;
    }

    //儿子吃饭
    public synchronized void consume() {
        if (flag) {
            try {
                this.wait();
            } catch (InterruptedException e) {
                e.printStackTrace();
            }
        }
        System.out.println("儿子吃了:" + food);
        //通知妈妈做饭
        this.notifyAll();
        this.flag = !flag;
    }
}

运行结果

妈妈做了:米饭
儿子吃了:米饭
妈妈做了:面条
儿子吃了:面条
妈妈做了:米饭
儿子吃了:米饭
妈妈做了:面条
儿子吃了:面条
...
妈妈做了:米饭
儿子吃了:米饭
妈妈做了:面条
儿子吃了:面条

Process finished with exit code 0

学习来源:kuangstudy

posted @ 2022-02-05 13:19  sxkio  阅读(48)  评论(0编辑  收藏  举报