多线程学习

简介

一个进程有多个线程

  • 程序:程序是指令和数据的有序集合,其本身没有运行的含义,是一个静态的概念
  • 进程:进程则是执行程序一次的过程,是个动态的概念,是系统资源分配的单位
  • 线程:通常一个进程中有若干个线程,线程是CPU调度和执行的单位

重要概念

  • 线程是独立执行的路径
  • 在程序执行时,自己即使没有创建线程,也会有多个线程,如主线程、gc线程
  • main()为主线程,程序入口,用于执行整个程序
  • 在一个进程中,若开辟了多个线程,线程的运行和调度由调度器安排,与操作系统有关,不能认为干预
  • 对于同一份操作资源,需加入并发控制
  • 线程会带来额外开销,如cpu调度时间,并发控制开销
  • 每个线程在自己的工作内存交互,内存控制不当会造成数据不一致

线程创建

三种创建方式:

  • 继承Thread类
  • 实现Runnable接口
  • 实现Callable接口

Thread类

  1. 自定义线程类继承Thread类
  2. 重写run()方法,编写线程执行体
  3. 创建线程对象,使用start()开启线程

TestThread1.java

package com.thread.demo1;

import com.oop.demo9.Test;

//Thread创建线程:继承Thread     重写run()方法      start()开启线程
public class TestThread1 extends Thread {
    //重写run()方法
    @Override
    public void run() {
        for (int i = 0; i < 20; i++) {
            System.out.println("TestThread1 " + i);
        }
    }

    public static void main(String[] args) {

        TestThread1 testThread1 = new TestThread1();
        testThread1.start();

        for (int i = 0; i < 20000; i++) {
            System.out.println("main " + i);
        }
    }
}

可发现主线程和新开辟的线程交替执行,结合简介中的图理解

线程开启后不一定开始执行,由CPU调度执行

从网络上下载图片例子

1、需要下载commons-io-2.8.0.jar,放到com.lib包下,然后add as Library

然后在Project Structure可找到加入的内容

2、TestThread2.java

package com.thread.demo1;

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() {
        new WebDownloader().downLoader(url, name);
        System.out.println("下载的文件名为:" + name);
    }

    public static void main(String[] args) {
        TestThread2 t1 = new TestThread2("https://images.cnblogs.com/cnblogs_com/sgKurisu/1928850/o_210206095542IDEA%E4%B8%8B%E8%BD%BD.JPG", "1.JPG"); //注意构造方法
        TestThread2 t2 = new TestThread2("https://images.cnblogs.com/cnblogs_com/sgKurisu/1928850/o_210206095542IDEA%E4%B8%8B%E8%BD%BD.JPG", "2.JPG"); //注意构造方法
        TestThread2 t3 = new TestThread2("https://images.cnblogs.com/cnblogs_com/sgKurisu/1928850/o_210206095542IDEA%E4%B8%8B%E8%BD%BD.JPG", "3.JPG"); //注意构造方法
        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异常");
        }
    }
}

结果:

线程执行的顺序不是根据代码所写的顺序执行,而是由CPU调度

注意,下载的文件位置,可发现下载的图片和项目为同一级

Runnable接口

  1. 定义MyRunnable类实现Runnable接口
  2. 实现run()方法,编写线程体
  3. 创建线程对象,代理,start()方法启动线程

TestThread3.java

package com.thread.demo1;

//Runnable接口创建线程
public class TestThread3 implements Runnable {
    @Override
    public void run() {
        //线程方法体
        for (int i = 0; i < 20; i++) {
            System.out.println("TestThread3 " + 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 < 2000; i++) {
            System.out.println("main " + i);
        }
    }

}

同样,用Runnable实现图片下载

package com.thread.demo1;

import org.apache.commons.io.FileUtils;

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

//联系Runnable,实现多线程同步下载图片
public class TestThread4 implements Runnable {

    private String url; //图片网址
    private String name; //保存的文件名

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

    @Override
    public void run() {
        new WebDownloader2().downLoader(url, name);
        System.out.println("下载的文件名为:" + name);
    }

    public static void main(String[] args) {
        TestThread2 t1 = new TestThread2("https://images.cnblogs.com/cnblogs_com/sgKurisu/1928850/o_210206095542IDEA%E4%B8%8B%E8%BD%BD.JPG", "1.JPG"); //注意构造方法
        TestThread2 t2 = new TestThread2("https://images.cnblogs.com/cnblogs_com/sgKurisu/1928850/o_210206095542IDEA%E4%B8%8B%E8%BD%BD.JPG", "2.JPG"); //注意构造方法
        TestThread2 t3 = new TestThread2("https://images.cnblogs.com/cnblogs_com/sgKurisu/1928850/o_210206095542IDEA%E4%B8%8B%E8%BD%BD.JPG", "3.JPG"); //注意构造方法

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

}

//下载器
class WebDownloader2 {
    //下载方法
    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异常");
        }
    }
}

Thread和Runnable对比:

Tread类

  • 继承Thread类来实现多线程
  • 启动:子类对象.start()
  • 不建议使用,避免单继承局限性

Runnable类

  • 接口Runnable实现多线程
  • 启动:new Thread(目标对象).start(),采用代理
  • 推荐使用,避免单继承局限性

并发问题

TestThread5.java

package com.thread.demo1;

//买票程序,认识并发问题
public class TestThread5 implements Runnable{

    private int ticketNumber = 10;

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

    public static void main(String[] args) {
        TestThread5 testThread5 = new TestThread5();

        new Thread(testThread5, "A").start();
        new Thread(testThread5, "B").start();
        new Thread(testThread5, "C").start();

    }


}

多线程操作同一资源情况下,会造成数据混乱,并发问题

龟兔赛跑

Race.java

package com.thread.demo1;

//模拟龟兔赛跑
public class Race implements Runnable {

    //胜利者
    private static String winner;

    @Override
    public void run() {
        for (int i = 0; i <= 200; i++) {
            boolean flag = gameOver(i);
            if (flag) {
                break;
            }
            System.out.println(Thread.currentThread().getName() + " 跑了" + i + "步");
            if (Thread.currentThread().getName().equals("兔子")) {
                try {
                    Thread.sleep(1);
                } catch (InterruptedException e) {
                    e.printStackTrace();
                }
                //兔子速度较快
                i = i + 5;
            }
        }
    }

    //判断是否完成比赛
    private boolean gameOver (int step) {
        if (winner != null) {
            return true;
        }
        if (step >= 200) {
            winner = Thread.currentThread().getName();
            System.out.println("胜利者:" + winner);
            return true;
        }
        return false;
    }

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

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

    }

}

上述模拟通过定义两个线程来实现

Callable接口

  1. 实现Callable接口,需返回值类型

  2. 重写call方法,需抛出异常

  3. 创建目标对象

  4. 创建执行服务

     ExecutorService ser = Executors.newFixedThreadPool(3);
    
  5. 提交执行

    Future<Boolean> r1 = ser.submit(t1);
    Future<Boolean> r2 = ser.submit(t2);
    Future<Boolean> r3 = ser.submit(t3);
    

    submit提交一个实现Callable接口的任务,并且返回封装了异步计算结果的Future

  6. 获取结果

    boolean rs1 = r1.get();  //call方法的返回值类型,本例中已经写了return true
    boolean rs2 = r2.get();  //需要抛出异常
    boolean rs3 = r3.get();
    
  7. 关闭服务

    ser.shutdownNow();
    

不使用以上线程池的方法,使用以下方法也可实现

FutureTask<Integer> futureTask = new FutureTask<Integer>(new MyThread3());
new Thread(futureTask).start();

TestThread6.java

package com.thread.demo1;

import org.apache.commons.io.FileUtils;

import java.io.File;
import java.io.IOException;
import java.net.URL;
import java.util.concurrent.*;

//Callable,实现多线程同步下载图片
//返回值类型可以任意设置
public class TestThread6 implements Callable<Boolean> {

    private String url; //图片网址
    private String name; //保存的文件名

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

    //注意该方法的返回值的类型和Callable<返回值类型>相同
    @Override
    public Boolean call() {
        new WebDownloader3().downLoader(url, name);
        System.out.println("下载的文件名为:" + name);
        return true;
    }

    public static void main(String[] args) throws ExecutionException, InterruptedException {
        TestThread6 t1 = new TestThread6("https://images.cnblogs.com/cnblogs_com/sgKurisu/1928850/o_210206095542IDEA%E4%B8%8B%E8%BD%BD.JPG", "1.JPG"); //注意构造方法
        TestThread6 t2 = new TestThread6("https://images.cnblogs.com/cnblogs_com/sgKurisu/1928850/o_210206095542IDEA%E4%B8%8B%E8%BD%BD.JPG", "2.JPG"); //注意构造方法
        TestThread6 t3 = new TestThread6("https://images.cnblogs.com/cnblogs_com/sgKurisu/1928850/o_210206095542IDEA%E4%B8%8B%E8%BD%BD.JPG", "3.JPG"); //注意构造方法

        //创建执行服务
        ExecutorService ser = Executors.newFixedThreadPool(3); //创建线程池
        //提交执行
        Future<Boolean> r1 = ser.submit(t1);
        Future<Boolean> r2 = ser.submit(t2);
        Future<Boolean> r3 = ser.submit(t3);
        //获取结果
        boolean rs1 = r1.get();  //call方法的返回值类型,本例中已经写了return true
        boolean rs2 = r2.get();  //需要抛出异常
        boolean rs3 = r3.get();
        //关闭服务
        ser.shutdownNow();
    }

}

//下载器
class WebDownloader3 {
    //下载方法
    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异常");
        }
    }
}

静态代理模式

  • 真实对象和代理对象需实现同一接口
  • 代理对象要代理真实角色

优点:

  • 真实对象做自己事件
  • 代理对象可做真实对象其他事件
new Thread(()-> System.out.println("ABC")).start();

可开启一个线程,实现Runnable接口,run方法中的代码为

System.out.println("ABC");

以结婚,婚庆公司为例介绍静态代理:

package com.thread.staticproxy;

//静态代理模式理解
public class StaticProxy {

    public static void main(String[] args) {
        /*
        WeddingCompany weddingCompany = new WeddingCompany(new You());
        weddingCompany.HappyMarry();
        */
        new Thread(()-> System.out.println("ABC")).start(); //开启一个线程,run()方法中的代码为 System.out.println("ABC")
        new WeddingCompany(new You()).HappyMarry();
    }

}

interface Marry {
    void HappyMarry();
}

//真实角色
class You implements Marry {

    @Override
    public void HappyMarry() {
        System.out.println("You结婚");
    }
}

//代理角色
class WeddingCompany implements Marry {

    private Marry target;

    public WeddingCompany(Marry target) {
        this.target = target;
    }

    @Override
    public void HappyMarry() {
        before();
        this.target.HappyMarry();
        after();
    }

    private void after() {
        System.out.println("结婚后");
    }

    private void before() {
        System.out.println("结婚前");
    }
}

Lamda表达式

  • 避免匿名内部类定义过多
  • 使代码简洁
  • 只留下核心逻辑

函数式接口:只包含一个抽象方法的接口

对于函数式接口,可通过Lamda表达式来创建该接口的对象

Lamda表达式属于函数式编程概念

TestLamda1.java

package com.thread.lamda;

public class TestLamda1 {

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

    public static void main(String[] args) {
        ILike iLike1 = new Like();
        iLike1.lamda();

        //静态内部类
        ILike iLike2 = new Like2();
        iLike2.lamda();

        //局部内部类
        class Like3 implements ILike {
            @Override
            public void lamda() {
                System.out.println("局部内部类");
            }
        }
        ILike iLike3 = new Like3();
        iLike3.lamda();

        //匿名内部类,必须借用接口或父类
        ILike ilike4 = new ILike() {
            @Override
            public void lamda() {
                System.out.println("匿名内部类");
            }
        };
        ilike4.lamda();

        //Lamda表达式
        ILike ilike5 = ()->{
            System.out.println("Lamda表达式");
        };
        ilike5.lamda();

    }
}

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

//实现类
class Like implements ILike {

    @Override
    public void lamda() {
        System.out.println("Like");
    }

}

TestLamda2.java

package com.thread.lamda;

//Lamda表达式简化
//若为多参数类型,去掉参数类型时,则需一起去掉,必须加括号
public class TestLamda2 {
    public static void main(String[] args) {
        //Lamda表达式
        ILove love = (int a)->{
            System.out.println("Lamda表达式 " + a);
        };
        love.iLove(2);
        
        //简化:去掉参数类型
        //通常用这种类型
        love = (a)->{
            System.out.println("Lamda表达式 " + a);
        };
        love.iLove(2);

        //简化:去掉括号
        love = a -> {
            System.out.println("Lamda表达式 " + a);
        };
        love.iLove(2);

        //简化:去掉花括号
        love = a -> System.out.println("Lamda表达式 " + a);  //本例代码仅为一行,故可省略花括号;若为多行,则不可简化花括号,应为代码块
        love.iLove(2);
    }

}

interface ILove {
    void iLove(int a);
}

在Lamda表达式中

for (int i = 0; i < 10000; i++) {
            int a = i;
            new Thread(()->{
                //list1.add(i);在Lamda表达式中应按照以下方式
                list1.add(a);
            }).start();
}

线程停止

根据“老猫1226”博客线程的5种状态详解

线程方法:

setPriority(int newPriority) //改变线程优先级
static void sleep(long millis) //线程休眠,单位毫秒
void join() //插队
static void yield() //暂停该线程,执行其他线程
boolean isAlive() //测试该线程是否处于活动状态

线程停止不推荐使用JDK提供的stop()和destory()方法,推荐使用一个标志位,来终止线程

TestStop.java

package com.thread.state;

import com.oop.demo9.Test;

public class TestStop implements Runnable {

    //1、设置一标志位flag
    boolean flag = true;

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

    //2、设置方法转换标志位
    public void stop() {
        this.flag = false;
    }

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

        for (int i = 0; i < 1000; i++) {
            if (i == 900) {
                testStop.stop();
                System.out.println("Thread Stop===============");
            }
            System.out.println("main " + i);
        }
    }
}

线程休眠

  • sleep阻塞当前线程的毫秒数
  • sleep存在InterruptedException异常
  • sleep可模拟倒计时,网络延迟等
  • 每个对象都有一个锁,sleep不会释放锁

TestSleep.java

package com.thread.state;

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

public class TestSleep {
    //十秒倒计时
    public void tenDown () throws InterruptedException {
        int num = 10;
        while (true) {
            if (num == 0) {
                break;
            }
            System.out.println(num);
            Thread.sleep(1000);
            num--;
        }
    }

    public static void main(String[] args) throws InterruptedException {
        new TestSleep().tenDown();

        //打印当前系统时间
        Date startTime = new Date(System.currentTimeMillis()); //获得当前系统时间
        //打印十次
        for (int i = 0; i < 10; i++) {
            Thread.sleep(1000);
            System.out.println(new SimpleDateFormat("HH:mm:ss").format(startTime));
            startTime = new Date(System.currentTimeMillis());
        }
    }
}

获取当前系统时间

//System.currentTimeMillis()来获取自1970-1-01 00:00:00.000到当前系统时间的毫秒数,类型为long
Date startTime = new Date(System.currentTimeMillis());
SimpleDateFormat sdf = new SimpleDateFormat("dd-MM-yyyy HH:mm:ss");
System.out.println(sdf.format(startTime));
//用Date startTime = new Date();代替Date startTime = new Date(System.currentTimeMillis());也可以输出当前时间

new Date()和System.currentTimeMillis()获取当前时间戳区别,参考“No Silver Bullet”的博客Java进阶(十六)使用new Date()和System.currentTimeMillis()获取当前时间戳 可知,查看java源码

new Date()所做的事情其实就是调用了System.currentTimeMillis()

线程礼让

让CPU重新调度,礼让不一定成功

TestYield.java

package com.thread.state;

public class TestYield {

    public static void main(String[] args) {
        /*
        MyYield myYield = new MyYield();
        new Thread(myYield, "a").start();
        new Thread(myYield, "b").start();
        */
        
        new Thread(new MyYield(), "a").start();
        new Thread(new 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() + "线程结束");
    }
}

礼让成功

礼让失败

线程强制执行

强制执行该线程,直到该线程结束

TestJoin.java

package com.thread.state;

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 {
        Thread thread = new Thread(new TestJoin());
        thread.start();

        //主线程
        for (int i = 0; i < 100; i++) {
            if (i == 50) {
                thread.join();
            }
            System.out.println("主线程 " + i);
        }
    }
}

观测线程状态

根据java帮助文档

六种状态区别参考线程的六种状态(Runnable、Blocked、Waiting、TimedWating详解

线程一旦死亡,不可再次启动start()

TestState.java

package com.thread.state;

//观测线程状态
public class TestState {
    public static void main(String[] args) {
        Thread thread = new Thread(()->{
            for (int i = 0; i < 5; i++) {
                try {
                    Thread.sleep(100);
                } catch (InterruptedException e) {
                    e.printStackTrace();
                }
            }
            System.out.println("======================");
        });
        /*Thread.State state = thread.getState();
        System.out.println(state);*/
        System.out.println(thread.getState());

        //启动
        thread.start();

        while (thread.getState() != Thread.State.TERMINATED) {
            System.out.println(thread.getState());
        }
        System.out.println(thread.getState());
    }

}

线程优先级

获取或改变优先级

getPriority()
setPriority(int xx)

TestPriority.java

package com.thread.state;

public class TestPriority {

    public static void main(String[] args) {
        //主线程默认优先级
        System.out.println(Thread.currentThread().getName() + "-->" + Thread.currentThread().getPriority());
        MyPriority myPriority = new MyPriority();

        Thread t1 = new Thread(myPriority, "t1");
        Thread t2 = new Thread(myPriority, "t2");
        Thread t3 = new Thread(myPriority, "t3");
        Thread t4 = new Thread(myPriority, "t4");

        //先设置优先级,再启动
        t1.start();

        t2.setPriority(1);
        t2.start();

        t3.setPriority(4);
        t3.start();

        t4.setPriority(Thread.MAX_PRIORITY);  //Thread.MAX_PRIORITY=10
        t4.start();

    }

}

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

可发现,优先级低意味着获得调度概率较低,而不是一定按照线程优先级的顺序来执行

守护线程

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

TestDaemo.java

package com.thread.state;

public class TestDaemo {
    public static void main(String[] args) {
        Test1 test1 = new Test1();
        Test2 test2 = new Test2();

        Thread t1 = new Thread(test1, "test1");
        t1.setDaemon(true);  //设置为守护线程,默认为false
        t1.start();

        new Thread(test2, "test2").start();
    }
}

class Test1 implements Runnable {

    @Override
    public void run() {
        while (true) {
            System.out.println("Daemo");
        }
    }
}

class Test2 implements Runnable {
    @Override
    public void run() {
        for (int i = 0; i < 1000; i++) {
            System.out.println(Thread.currentThread().getName());
        }
        System.out.println("==================Test2结束=================");
    }
}

线程同步

并发:同一个对象被多个线程同时操作

多线程问题时,多个线程同时操作一个对象,并且某些线程还想修改这个对象,就需要线程同步。

线程同步其实是一种等待机制,多个需要访问此对象的线程进入到该对象的等待池,形成队列,等待前面线程执行完后下一个线程再使用。

队列+锁,构成线程同步的安全性,由性能代价换来安全性

锁机制存在问题:

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

三大不安全案例

1、多线程买票问题

UnsafeBuyTicket.java

package com.thread.syn;

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

        new Thread(buyTicket, "A").start();
        new Thread(buyTicket, "B").start();
        new Thread(buyTicket, "C").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;
        }
        try {
            Thread.sleep(100);
        } catch (InterruptedException e) {
            e.printStackTrace();
        }
        System.out.println(Thread.currentThread().getName() + "买到第" + ticketNum-- + "张票");

    }

}

2、模拟银行取钱问题

package com.thread.syn;

//分别采用了Thread和Runnable创建线程方法实现此问题
public class UnsafeBank {
    public static void main(String[] args) {
        Account account = new Account("Account", 100); //新建一个账户

       /* Drawing d1 = new Drawing(account, 50, "A");
        Drawing d2 = new Drawing(account, 100, "B");

        d1.start();
        d2.start();*/
        new Thread(new Drawing(account, 50), "A").start();
        new Thread(new Drawing(account, 100), "B").start();
    }

}

//账户
class Account {
    int money;      //余额
    String name;    //名称

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

//银行:模拟取款
class Drawing implements Runnable {
    Account account; //账户
    int drawingMoney; //取出金额
    int nowMoney; //手中剩余金额

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

    @Override
    public void run() {
        //判断是否可取
        if (account.money < drawingMoney) {
            System.out.println(Thread.currentThread().getName() + "钱不够,不能取出");
            return;
        }
        try {
            Thread.sleep(100);
        } catch (InterruptedException e) {
            e.printStackTrace();
        }
        account.money = account.money - drawingMoney;
        nowMoney = nowMoney + drawingMoney;

        System.out.println(Thread.currentThread().getName() + "手中钱" + nowMoney);
        System.out.println(account.name + "余额" + account.money);
    }
}
/*class Drawing extends Thread {
    Account account; //账户
    int drawingMoney; //取出金额
    int nowMoney; //手中剩余金额

    public Drawing (Account account, int drawingMoney, String name) {
        super(name); //线程名字
        this.account = account;
        this.drawingMoney = drawingMoney;
    }

    //取钱
    @Override
    public void run() {
        //判断是否可取
        if (account.money < drawingMoney) {
            System.out.println(Thread.currentThread().getName() + "钱不够,不能取出");
            return;
        }
        try {
            Thread.sleep(100);
        } catch (InterruptedException e) {
            e.printStackTrace();
        }
        account.money = account.money - drawingMoney;
        nowMoney = nowMoney + drawingMoney;

        //this.getName() = Thread.currentThread().getName()
        System.out.println(this.getName() + "手中钱" + nowMoney);
        System.out.println(account.name + "余额" + account.money);

    }
}*/

3、线程不安全集合

package com.thread.syn;

import java.util.ArrayList;
import java.util.List;

public class UnsafeList {
    public static void main(String[] args) {
        List<String> list = new ArrayList<String>();
        for (int i = 0; i < 10000; i++) {
            new Thread(()->{
                list.add(Thread.currentThread().getName());
            }).start();
        }
        try {
            Thread.sleep(10);
        } catch (InterruptedException e) {
            e.printStackTrace();
        }
        System.out.println(list.size());
    }
}

可发现list.size()并未达到10000

同步方法及同步块

同步方法,通过synchronized关键字,包括synchronized方法和synchronized块

synchronized来控制对象的访问,每个对象都有一把锁,每个synchronized方法必须获得该方法对象的锁才能执行,否则会阻塞。方法一旦执行,就会独占该锁,知道该方法结束,后面线程才能获得该锁。

方法中需要修改的内容才需要加锁,否则将会浪费资源

同步块:synchronized (Obj){}

  • Obj称为同步监视器,可以为任何对象,推荐使用共享资源,为变化的量
  • 同步方法中的监视器就为this,对象本身

对上节中的三个例子进行修改:

1、买票模拟中,仅需在买票方法前加synchronized关键字

//synchronized变为同步方法
private synchronized void buy() {
    //判断是否有票
    if (ticketNum <= 0) {
        flag = false;
        return;
    }
    try {
        Thread.sleep(100);
    } catch (InterruptedException e) {
        e.printStackTrace();
    }
    System.out.println(Thread.currentThread().getName() + "买到第" + ticketNum-- + "张票");

}

2、银行取钱模拟中,使用synchronized同步块,同步监视器为变化的account

package com.thread.syn;

//分别采用了Thread和Runnable创建线程方法实现此问题
public class UnsafeBank {
    public static void main(String[] args) {
        Account account = new Account("Account", 100); //新建一个账户

       /* Drawing d1 = new Drawing(account, 50, "A");
        Drawing d2 = new Drawing(account, 100, "B");

        d1.start();
        d2.start();*/
        new Thread(new Drawing(account, 50), "A").start();
        new Thread(new Drawing(account, 100), "B").start();
    }

}

//账户
class Account {
    int money;      //余额
    String name;    //名称

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

//银行:模拟取款
class Drawing implements Runnable {
    Account account; //账户
    int drawingMoney; //取出金额
    int nowMoney; //手中剩余金额

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

    @Override
    public void run() {
        //锁的为变化的量
        synchronized (account) {
            //判断是否可取
            if (account.money < drawingMoney) {
                System.out.println(Thread.currentThread().getName() + "钱不够,不能取出");
                return;
            }
            try {
                Thread.sleep(100);
            } catch (InterruptedException e) {
                e.printStackTrace();
            }
            account.money = account.money - drawingMoney;
            nowMoney = nowMoney + drawingMoney;

            System.out.println(Thread.currentThread().getName() + "手中钱" + nowMoney);
            System.out.println(account.name + "余额" + account.money);
        }
    }
}
/*class Drawing extends Thread {
    Account account; //账户
    int drawingMoney; //取出金额
    int nowMoney; //手中剩余金额

    public Drawing (Account account, int drawingMoney, String name) {
        super(name); //线程名字
        this.account = account;
        this.drawingMoney = drawingMoney;
    }

    //取钱
    @Override
    public void run() {
        //判断是否可取
        if (account.money < drawingMoney) {
            System.out.println(Thread.currentThread().getName() + "钱不够,不能取出");
            return;
        }
        try {
            Thread.sleep(100);
        } catch (InterruptedException e) {
            e.printStackTrace();
        }
        account.money = account.money - drawingMoney;
        nowMoney = nowMoney + drawingMoney;

        //this.getName() = Thread.currentThread().getName()
        System.out.println(this.getName() + "手中钱" + nowMoney);
        System.out.println(account.name + "余额" + account.money);

    }
}*/

3、ArrayList集合

package com.thread.syn;

import java.util.ArrayList;
import java.util.List;

public class UnsafeList {
    public static void main(String[] args) {
        List<String> list = new ArrayList<String>();
        for (int i = 0; i < 10000; i++) {
            new Thread(()->{
                synchronized (list) {
                    list.add(Thread.currentThread().getName());
                }
            }).start();
        }
        try {
            Thread.sleep(10);
        } catch (InterruptedException e) {
            e.printStackTrace();
        }
        System.out.println(list.size());
    }
}

还可能会出现并发问题,可使用Collections中的同步方法

List<String> list = Collections.synchronizedList(new ArrayList<>());

CopyOnWriteArrayList

根据“住手丶让我来”的博客高并发编程之CopyOnWriteArrayList介绍

  • 支持高效率并发且是线程安全的,读操作无锁的ArrayList,所有可变操作都是通过对底层数组进行一次新的复制来实现。
  • CopyOnWriteArrayList 合适读多写少的场景,CopyOnWriteArrayList中写操作需要大面积复制数组,所以性能肯定很差。
package com.thread.syn;

import java.util.concurrent.CopyOnWriteArrayList;

//测试JUC安全性
public class TestJUC {
    public static void main(String[] args) {
        CopyOnWriteArrayList<String> list = new CopyOnWriteArrayList<String>();
        for (int i = 0; i < 10000; i++) {
            new Thread(()->{
                list.add(Thread.currentThread().getName());
            }).start();
        }
        try {
            Thread.sleep(10);
        } catch (InterruptedException e) {
            e.printStackTrace();
        }
        System.out.println(list.size());
    }
}

经测试线程是安全同步的

死锁

多个线程各自占有一些共享资源,并且互相等待对方释放资源后才能运行,导致两个或多个线程都在等待对方释放资源

package com.thread.lock;

//死锁:多个线程相互持有对方需要资源,僵持
public class DeadLock {
    public static void main(String[] args) {
        AB a1 = new AB(0, "a1");
        AB a2 = new AB(1, "a2");//互相需要对方资源

        a1.start();
        a2.start();
    }
}

class A {

}

class B {

}

class AB extends Thread {
    //用static来保证每份资源仅有一份
    static A a = new A();
    static B b = new B();

    int choice;
    String name;

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

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

    private void make() throws InterruptedException {
        if (choice == 0) {
            synchronized (a) { //获得A的锁
                System.out.println(this.name + "获得A");
                Thread.sleep(1000);
                synchronized (b) {
                    System.out.println(this.name + "获得B");
                }
            }
        } else {
            synchronized (b) { //获得A的锁
                System.out.println(this.name + "获得B");
                Thread.sleep(1000);
                synchronized (a) {
                    System.out.println(this.name + "获得A");
                }
            }
        }
    }
}

导致陷入死锁

对make()方法进行修改:

private void make() throws InterruptedException {
    if (choice == 0) {
        synchronized (a) { //获得A的锁
            System.out.println(this.name + "获得A");
            Thread.sleep(1000);
        }
        synchronized (b) {
            System.out.println(this.name + "获得B");
        }
    } else {
        synchronized (b) { //获得A的锁
            System.out.println(this.name + "获得B");
            Thread.sleep(1000);
        }
        synchronized (a) {
            System.out.println(this.name + "获得A");
        }
    }
}

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

  • 互斥条件:一个资源被一个进程占用
  • 请求与保持条件:一个资源请求对方资源陷入等待,另一个线程不释放资源
  • 不剥夺条件:进程获得的资源在未释放之前不可被强行剥夺
  • 循环等待条件:若干进程间形成循环等待关系

Lock锁

java.util.concurrent.locks.Lock接口来控制多个线程对共享资源进行访问

ReentrantLock类实现了Lock,拥有与synchronized相同的并发性,常用的是ReentrantLock,来进行显示加锁,释放锁

private final ReentrantLock lock = new ReentrantLock();
try {
	lock.lock();
	需要加锁的代码
} finally {
	lock.unlock();
}
package com.thread.lock;

import java.util.concurrent.locks.ReentrantLock;

//用买票程序测试Lock锁
public class TestLock {
    public static void main(String[] args) {
        Ticket ticket = new Ticket();

        new Thread(ticket, "A").start();
        new Thread(ticket, "B").start();
        new Thread(ticket, "C").start();

    }
}

class Ticket implements Runnable {

    int ticketNum = 10;
    private final ReentrantLock lock = new ReentrantLock();

    @Override
    public void run() {
        while (true) {
            //使用try……finally加锁
            try {
                lock.lock();
                if (ticketNum <= 0) {
                    break;
                }
                try {
                    Thread.sleep(100);
                } catch (InterruptedException e) {
                    e.printStackTrace();
                }
                System.out.println(Thread.currentThread().getName() + "获得第" + ticketNum-- + "张票");
            } finally {
                lock.unlock();
            }

        }
    }
}

synchronized和Lock对比

  • Lock是显示锁,需要手动开锁和关锁,而synchronized是隐式锁
  • 锁,出了作用区域后自动释放
  • Lock只有代码块锁,而synchronized有代码块锁和方法锁

生产者消费者问题

这是个线程同步问题,生产者和消费者共享同一资源,相互依赖,互为条件。

仅用synchronized不够,synchronized不能够实现线程通信

线程间通信问题,可用以下方法

  • wait():线程等待,等待其他线程唤醒
  • notify():唤醒处于等待状态的线程,当线程较多时,被唤醒的线程是随机选择的
  • notifyall():唤醒一个对象上所有调用wait()方法的线程,优先级高的调用

以上方法均为Object类的方法,仅能在同步方法或同步代码块中使用

管程法

package com.producer;

//管程法测试生产者消费者问题
//生产者,消费者,缓冲区,产品
public class TestPC {
    public static void main(String[] args) {
        SynContainer container = new SynContainer();
        new Thread(new Producer(container),"生产者").start();
        new Thread(new Consumer(container),"消费者").start();
    }
}

//生产者
class Producer implements Runnable {
    SynContainer container = new SynContainer();
    public Producer(SynContainer container) {
        this.container = container;
    }

    @Override
    public void run() {
        //生产
        for (int i = 0; i < 100; i++) {
            container.push(new Chicken(i));
            System.out.println("生产了第" + i + "只鸡");
        }
    }
}

//消费者
class Consumer implements Runnable {
    SynContainer container = new SynContainer();
    public Consumer (SynContainer container) {
        this.container = container;
    }

    @Override
    public void run() {
        //消费
        for (int i = 0; i < 100; i++) {
            System.out.println("消费了第" + container.pop().id + "只鸡");
        }
    }

}

//产品
class Chicken {
    int id;

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

//缓冲区
class SynContainer {
    //容器大小
    Chicken[] chickens = new Chicken[10]; //容器大小为10
    //容器计数器
    int count = 0;

    //生产者放入产品
    public synchronized void push (Chicken chicken) { //一次放入一个
        //若容器满了,则通知消费者消费
        if (count == chickens.length) {
            //通知消费者消费,生产等待
            try {
                this.wait();
            } catch (InterruptedException e) {
                e.printStackTrace();
            }
        }
        chickens[count] = chicken;
        count++;
        //有产品,通知消费者消费
        this.notifyAll();
    }

    //消费者消费产品
    public synchronized Chicken pop () {
        //判断是否可以消费
        if (count == 0) {
            //生产者生产,消费者等待
            try {
                this.wait();
            } catch (InterruptedException e) {
                e.printStackTrace();
            }
        }
        //取出chicken
        count--;
        Chicken chicken = chickens[count];
        //通知生产者生产
        this.notifyAll();
        //返回取出的chicken
        return chicken;
    }

}

信号灯法

使用标志位来进行线程间通信调度

package com.producer;

//信号灯法,使用标志位
public class TestPC2 {
    public static void main(String[] args) {
        TV tv = new TV();

        new Player(tv).start();
        new Watcher(tv).start();
    }
}

//生产者
class Player extends Thread {
    TV tv;
    public Player (TV tv) {
        this.tv = tv;
    }
    
    @Override
    public void run() {
        for (int i = 0; i < 20; i++) {
            if (i%2 == 0) {
                this.tv.play("节目A");
            } else {
                this.tv.play("节目B");
            }
        }
    }
}
//消费者
class Watcher extends Thread {
    TV tv;
    public Watcher (TV tv) {
        this.tv = tv;
    }

    @Override
    public void run() {
        for (int i = 0; i < 20; i++) {
            tv.watch();
        }
    }
}
//产品
class TV {
    //演员表演,观众等待  T
    //观众观看,演员等待  F
    String voice;  //节目
    boolean flag = true;

    //表演
    public synchronized void play(String voice) {
        if (!flag) {
            try {
                this.wait();
            } catch (InterruptedException e) {
                e.printStackTrace();
            }
        }
        System.out.println("演员表演了:" + voice);
        this.notifyAll();
        this.voice = voice;
        this.flag = !this.flag;
    }
    //观看
    public synchronized void watch() {
        if (flag) {
            try {
                this.wait();
            } catch (InterruptedException e) {
                e.printStackTrace();
            }
        }
        System.out.println("观众观看:" + voice);
        //观看结束后通知演员表演
        this.notifyAll();
        this.flag = !this.flag;
    }

}

线程池

提前创建多个线程,放到线程池中,使用时获取,用完后放入到线程池中,可避免频繁创建销毁线程,提高利用率

优点:

  • 提高响应速度
  • 降低资源消耗
  • 便于线程管理
    • corePoolSize:核心池大小
    • maximumPoolSize:最大线程数
    • keepAliveTime:线程没有任务时最多保持多长时间后会终止

相关类和接口

  • ExecutorService:线程池接口
    • void execute(Runnable command):用来执行任务或命令,无返回值
    • Future submit(Callable task):执行任务,有返回值
    • void shutdown():关闭线程池
  • Executors:线程池工厂类,用于创建和返回不同类型的线程池

总结

线程的三种创建方式

package com.thread;

import java.util.concurrent.Callable;
import java.util.concurrent.ExecutionException;
import java.util.concurrent.FutureTask;

public class ThreadInclude {
    public static void main(String[] args) {
        new MyThread1().start();

        new Thread(new MyThread2()).start();

        FutureTask<Integer> futureTask = new FutureTask<Integer>(new MyThread3());
        new Thread(futureTask).start();
        try {
            System.out.println(futureTask.get());
        } catch (InterruptedException e) {
            e.printStackTrace();
        } catch (ExecutionException e) {
            e.printStackTrace();
        }
    }
}

class MyThread1 extends Thread {
    @Override
    public void run() {
        System.out.println("MyThread1");
    }
}

class MyThread2 implements Runnable {
    @Override
    public void run() {
        System.out.println("MyThread2");
    }
}

class MyThread3 implements Callable<Integer> {

    @Override
    public Integer call() throws Exception {
        System.out.println("MyThread3");
        return 100;
    }
}
posted @ 2021-02-19 17:10  sgKurisu  阅读(82)  评论(0)    收藏  举报