多线程学习

线程介绍

原来的方法:只有主线程一条执行路径,调用其他方法时先处理其他方法,之后再回来继续主线程;

多线程:主线程中调用start()方法,使得子线程开始执行,此时主线程不必等待,而是与子线程并行交替执行

其他概念对比:

  1. 程序:静态概念,是指令和数据的有序集合。
  2. 进程:动态概念,是执行程序的一次过程,它代表系统资源分配的单位。
  3. 线程:包含在进程之中,一个进程可以包含若干个线程,线程则是CPU调度和执行的单位。

注:

  • 真正的多线程指多个CPU即多核处理;若是一个CPU模拟出来的多线程,在同一个时间点,CPU只能执行一个代码,但是线程之间互相切换很快,所以就显得是同时执行。
  • 线程不一定会执行,这个由CPU决定调度执行

线程创建

1.方式分类

  1. Thread class——继承Thread类
  2. Runnable接口——实现Runnable接口
  3. Callable接口——实现Callable接口

2.继承Thread类

步骤如下:

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

示例如下:

//继承Thread类
public class Demo01 extends Thread{
    //重写run()
    @Override
    public void run() {
        for (int i = 0; i < 10; i++) {
            System.out.println("hello");
        }
    }

    public static void main(String[] args) {
        Demo01 demo01 = new Demo01();
        demo01.start();//执行子线程
        for (int i = 0; i < 200; i++) {
            System.out.println("hi");
        }
    }
}

当执行次数较多,可以观察到子线程和主线程穿插输出。

以下为案例示范:

//案例,线程同时下载文件
public class Demo02 extends Thread{
    String url;
    String file;
	//构造方法初始化
    public Demo02(String url, String file) {
        this.url = url;
        this.file = file;
    }

    //重写run(),一个线程执行y下载方法
    @Override
    public void run() {
        Downloader downloader = new Downloader();
        downloader.download(url,file);
        System.out.println(file);
    }

    public static void main(String[] args) {
        //URL是什么可以自己试验
        Demo02 demo021 = new Demo02("https://*****","1.jpg");
        Demo02 demo022 = new Demo02("https://*****","2.jpg");
        Demo02 demo023 = new Demo02("https://*****","3.jpg");

        demo021.start();
        demo022.start();
        demo023.start();

    }
}

class Downloader{
    //下载方法
    public void download(String url, String file){
        try {
            FileUtils.copyURLToFile(new URL(url),new File(file));
        } catch (IOException e) {
            e.printStackTrace();
        }
    }
}

3.实现Runnable接口

大致方式为:创建一个实现类实现Runnable接口,重写run()方法,用Thread()有参构造引入实现类的对象,并调用Thread对象中的start()方法即可实现多线程。

public class Demo03 implements Runnable{
    //实现Runnable接口,重写run方法,最后调用start方法
    @Override
    public void run() {
        for (int i = 0; i < 10; i++) {
            System.out.println("hello");
        }
    }

    public static void main(String[] args) {
        Demo03 demo03 = new Demo03();
        new Thread(demo03).start();//调用start方法
        for (int i = 0; i < 200; i++) {
            System.out.println("hi");
        }
    }
}

4实现Callable接口(非重点应用)

方式类似,只是调用多线程服务时需要四步:

  • 创建执行服务
  • 提交执行
  • 获取结果
  • 关闭服务
public class Demo05 implements Callable {

    String url;
    String file;

    public Demo05(String url, String file) {
        this.url = url;
        this.file = file;
    }

    //重写run()
    @Override
    public Boolean call() {
        CallDownloader downloader = new CallDownloader();
        downloader.download(url,file);
        System.out.println(file);
        return true;
    }

    public static void main(String[] args) throws Exception{
        Demo05 demo021 = new Demo05("https://gimg2.baidu.com/***","1.jpg");
        Demo05 demo022 = new Demo05("https://gimg2.baidu.com/*****","2.jpg");
        Demo05 demo023 = new Demo05("https://gimg2.baidu.com/****","3.jpg");

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

        //提交执行
        Future<Boolean> r1 = ser.submit(demo021);
        Future<Boolean> r2 = ser.submit(demo022);
        Future<Boolean> r3 = ser.submit(demo023);

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

        System.out.println(rs1);
        System.out.println(rs2);
        System.out.println(rs3);

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

    }


}

class CallDownloader{
    //下载方法
    public void download(String url, String file){
        try {
            FileUtils.copyURLToFile(new URL(url),new File(file));
        } catch (IOException e) {
            e.printStackTrace();
        }
    }
}

静态代理

将对象区分为真实对象和代理对象。

  1. 他们继承同一个接口。
  2. 代理对象代理真实对象

所谓代理的意思就是,我代替其他人做那些做不了的事情,代理对象则就是去做真实对象做不了的事情。

如何实现?这里打个比方:

class A implements face//真实
{
    @override
    public void hhh(){
        //***
    }
}
class B implements face//代理
{
    face fa;
    public B(face fa){
        this.fa = fa;
    }
    @override
    public void hhh(){
        //***
        fa.hhh();
    }
}
interface face{
    public void hhh();
}

这里真实和代理都实现一个接口,但是两者的实现方法不同,且代理方法在实现过程中也实现真实对象的方法

最后在调用实现时:

new B(new A()).hhh();

就可以实现静态代理,这与多线程中:

new Thread(() -> System.out.println("")).start();

有异曲同工之妙

Lambda表达式

在上面说到多线程里面的时候有提到一个奇怪的表达式:

() -> System.out.println("").start()

这就是一种Lambda表达式

首先明确一个概念:函数式接口

任何接口,如果只包含一个抽象方法,那么就是一个函数式接口

当我们有了函数式接口才能用Lambda表达式:

public class Demo06 {
    //Lambda表达式
    public static void main(String[] args) {
        testFace tf = () -> System.out.println("hhh");
        tf.test();
    }
}
//函数式接口
interface testFace{
    public void test();
}

该式子是建立在匿名内部类基础上的改编,可以将其类比为:

new testFace(
    @override
    public void test(){
    	System.out.println("hhh");
})

详细解析一下:

1.()里面的是指该函数式接口的形参列表,如果形参没有就为空。如果只有一个形参可以不打括号,如果没有或者多个形参必须打括号。(形参数据类型可以省略)

2.箭头后面加大括号{},里面就是重写函数体,如果只有一句代码就不用打大括号,如上述例子所示。

线程状态相关

线程分为五个状态:创建态,就绪态,阻塞态,运行态,终止态

相关方法:

方法 说明
setPriority(int newPriority) 更改线程的优先级
static void sleep(long millis) 指定毫秒数内休眠该执行中的线程
void join() 等待该线程终止
static void yield() 暂停该执行中的线程,并执行其他线程,即线程礼让
void interrupt() 中断线程(不推荐)
boolean isAlive() 检查线程是否存活

线程停止

一般建议让线程自己停止,实在不行可以考虑使用一个外部标志位让线程停止:

boolean flag = true;

public void stop(){
    flag = false;
}

此时设立该flag变量作为外部标志位,当主线程和子线程一同变化的时候,在主线程中可以找机会使用stop()改变flag,以使得子线程停止运行。

线程休眠

Thread.sleep(int millis)方法,他存在异常InterruptedException,需要进行抛出。

注意:

  1. sleep方法会让线程进入阻塞态,进行完毕则进入就绪态
  2. sleep可以模拟网络延时,倒计时等
  3. 每个对象都有一个,sleep方法不会释放锁

线程礼让

礼让是让线程从运行转为就绪态,方法为Thread.yield()

礼让是不一定成功的!具体看CPU安排

线程强制执行

令其他线程阻塞,等待该线程执行完再执行其他线程,方法为join()

该方法不是静态方法

观测线程状态

getState()方法,观察线程状态,也不是静态方法

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

    //启动线程
    thread.start();
    //观察启动后线程的状态
    state = thread.getState();
    System.out.println(state);

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

}

线程的优先级

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

使用setPrioritygetPriority改变线程或者获取优先级

其中源码内几个静态常量:

public final static int MIN_PRIORITY = 1;//最小优先级

public final static int NORM_PRIORITY = 5;//中等优先级

public final static int MAX_PRIORITY = 10;//最大优先级

优先级高的越容易先执行,但是实际情况是CPU自己调动,只是调用概率高一点。

守护线程(daemon)

线程分为用户线程守护线程

虚拟机需要确保用户线程执行完毕,但是不用等待守护线程执行完毕。虚拟机确定用户线程执行完毕即可关闭,守护线程在关闭虚拟机之时也会自行关闭。

public class Demo08 {
    //守护线程
    public static void main(String[] args) {
        God god =new God();

        //设置守护线程
        Thread thread = new Thread(god);
        thread.setDaemon(true);//守护线程,默认false指用户线程

        //启动
        thread.start();//守护线程
        new Thread(() -> {
            for (int i = 0; i < 300; i++) {
                System.out.println("live is good!");
            }
            System.out.println("寄");
        }).start();//用户线程
    }
}

//守护线程类
class God implements Runnable{
    @Override
    public void run() {
        while (true){
            System.out.println("hi!!!!");
        }
    }
}

线程同步

即当有一个线程在对内存进行操作时,其他线程都不可以对这个内存地址进行操作,直到该线程完成操作, 其他线程才能对该内存地址进行操作,而其他线程又处于等待状态.

​ ——百度百科

要实现线程同步的安全性:队列 + 锁(synchronized

队列很简单理解,先入先出,类似于排队;锁则是每个线程都可以加的,开启锁之后,该线程独占资源,处理完该线程后释放锁,队列的下一个线程才能运行。

Synchronized

锁机制:

  1. 保证安全性,但是其他线程会挂起,丧失性能
  2. 优先级高的线程等待优先级低的线程释放锁,会导致优先级倒置,引起性能问题

要实现同步机制,就是要实现synchronized关键字,包括:同步方法和同步块。

要注意,synchronized关键字锁的对象必须是变化的量,例如:

  • 同步方法:锁住运行的方法,当其他的线程需要调用这个方法时要看是否被锁
  • 同步块:锁住某个对象,让块中的步骤在运行期间锁住该对象。

格式:

  • 同步方法:前面加个synchronized关键字即可
  • 同步块:synchronized(Obj)

注:

之前提到过ArrayList集合是不安全集合,在JUC包下有一个线程安全的对应集合类叫:CopyOnWriteArrayList。线程安全也就是指线程能够同步。

死锁

多个线程各自占有一些资源,并相互等待其他释放占有的资源。

在操作系统中,死锁必须满足四个条件:

  1. 互斥条件:某资源同一时间内只能由一个进程占有
  2. 不剥夺条件:资源未用完前,其他进程不能剥夺资源,只能由本进程自己释放。
  3. 请求并保持:进程已经占有至少一个资源,但又提出新的资源请求
  4. 循环等待条件:存在一种进程链,使得链中每个进程已获得的资源被下一个进程请求

在JAVA中表现为,某一个同步块同时拥有“两个以上对象的锁”,就会发生死锁问题。

Lock锁——显示锁

JUC包下的Lock接口是控制多个线程对共享资源进行访问的工具,提供了对共享资源的独占访问。

一般使用的实现类为:ReentrantLock——可重入锁,它拥有与Synchronized相同的并发性和内存语义。

public class TestLock {
    public static void main(String[] args) {
        TestLock2 lock2 = new TestLock2();
        new Thread(lock2).start();
        new Thread(lock2).start();
        new Thread(lock2).start();
    }
}

class TestLock2 implements Runnable{

    int ticketsNum = 10;

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

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

注意:为了保证同步代码的异常抛出,最好将unlock()解锁方法写入finally语句块,所以以上代码内有try finally块在内。

Lock与synchronized锁区别:

  1. 显式Lock锁手动开启和关闭,synchronized隐式开启,超出作用域自动释放。
  2. Lock只能锁代码块,synchronized除此之外有方法锁
  3. Lock花费更少时间调度进程,性能更好,且提供更多子类,有可扩展性。

优先使用Lock锁进行加锁,其次使用同步代码块,最次用同步方法加锁

线程通信

生产者消费者问题

这是一个线程同步问题,生产者消费者共享一个资源,并且生产者和消费者之间相互依赖,互为条件。在这里面还要处理一种线程通信的问题:

  • 对生产者,没有产品时要通知消费者等待;有产品时通知消费者消费
  • 对消费者,消费之后要通知生产者已经结束消费,需要生产新产品以供消费

在这种问题内仅有synchronized是不够的,因为不能实现线程的通信。

线程通信的方法:

  1. wait()——表示线程会等待,知道其他线程通知,与sleep方法不同的是它会释放锁

    wait(long timeout)——指定等待的毫秒数

  2. notify()——唤醒一个等待进程

    notifyAll()——唤醒同一对象上所有wait的线程,优先级高的先调度

解决消费者生产者问题的方法:

  1. 管程法

    新建一个缓冲区

    生产者用来生产数据,消费者用来处理数据,在生产者和消费者之间放入缓冲区,生产者将数据放入缓冲区,消费者从缓冲区取出数据。

  2. 信号灯法

    使用一种并发协作模型处理:生产者/消费者模式,使用一个判断标志位监听是否传递。

管程法

//生产者消费者问题
//管程法
public class Demo10 {
    public static void main(String[] args) {
        SynContainer container = new SynContainer();
        Producer producer = new Producer(container);
        Consumer consumer = new Consumer(container);

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

    //生产
    @Override
    public void run() {
        for (int i = 0; i < 100; i++) {
            container.synProducer(new Chicken(i));
            System.out.println("生产了第"+i+"只鸡");
        }
    }
}
//消费者
class Consumer implements Runnable{
    SynContainer container;
    public Consumer(SynContainer container){
        this.container = container;
    }
    
    //消费
    @Override
    public void run() {
        for (int i = 0; i < 100; i++) {
            Chicken chicken = container.synConsumer();
            System.out.println("消费了第"+chicken.id+"只鸡");

        }
    }
}
//产品
class Chicken{
    int id;//产品编号
    public Chicken(int id){
        this.id = id;
    }

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

    //生产者判断,记得加锁
    public synchronized void synProducer(Chicken chicken){
        //如果缓冲区满,就提醒消费者
        while (count == chickens.length){
            try {
                this.wait();
            } catch (InterruptedException e) {
                e.printStackTrace();
            }
        }
        //如果没满就加入
        chickens[count++] = chicken;
        this.notifyAll();
    }

    //消费者判断,记得加锁
    public synchronized Chicken synConsumer(){
        //如果缓冲区为空,则等待生产者生产
        while (count == 0){
            try {
                this.wait();
            } catch (InterruptedException e) {
                e.printStackTrace();
            }
        }
        //否则消费
        Chicken chicken = chickens[--count];
        this.notifyAll();
        return chicken;
    }

}

由于要实现线程同步,所以要加锁,但是不能使用sleep方法休眠程序,因为sleep休眠不释放锁,wait方法可以释放锁,从而让对方可以开始执行进程而不至于一直等待。

信号灯法

//生产者消费者问题
//信号灯法
//车辆通行,行人等待;车辆等待,行人通行
public class Demo11 {
    public static void main(String[] args) {
        Light light = new Light();
        new Thread(new Car(light)).start();
        new Thread(new Person(light)).start();

    }
}

class Car implements Runnable{
    Light light;
    public Car(Light light){
        this.light = light;
    }
    @Override
    public void run() {
        for (int i = 0; i < 30; i++) {
            light.ride();
        }
    }
}

class Person implements Runnable{
    Light light;
    public Person(Light light){
        this.light = light;
    }
    @Override
    public void run() {
        for (int i = 0; i < 30; i++) {
            light.walk();
        }
    }
}

//红绿灯
class Light{
    //true 行人通行
    //false 车辆通行
    boolean light = true;

    public synchronized void walk(){
        //如果false,就等待
        if (!light){
            try {
                wait();
            } catch (InterruptedException e) {
                e.printStackTrace();
            }
        }
        //如果true就通行
        notifyAll();
        System.out.println("行人通行");
        light = !light;

    }

    public synchronized void ride(){
        //如果true,就等待
        if (light){
            try {
                wait();
            } catch (InterruptedException e) {
                e.printStackTrace();
            }
        }
        //如果true就通行
        notifyAll();
        System.out.println("车辆通行");
        light = !light;
    }
}

线程池

提前创建好多个线程,放入线程池中,使用时直接获取,使用完放回,避免重复创建销毁,实现重复利用。

  • 可以提高响应速度
  • 降低资源消耗
  • 便于管理:如线程池大小,最大线程数,线程池一起销毁判断

线程池接口:ExcutorService,常见子类ThreadPoolExcutor

方法如下:

  • void excutor(Runnable command):执行任务/命令,没有返回值,一般用作执行Runnable
  • <T> Future<T> submit(Callable<T> task):一般用来执行Callable,有返回值

Excutors则是一种工具类,用于创建并返回不同类型的线程池:

public class Demo12 {
    public static void main(String[] args) {
        //创建线程池
        ExecutorService service = Executors.newFixedThreadPool(10);//参数为池子大小

        //线程加入池子并执行
        service.execute(new MyThread());
        service.execute(new MyThread());
        service.execute(new MyThread());
        service.execute(new MyThread());

        //关闭链接
        service.shutdown();
    }
}

class MyThread implements Runnable{
    @Override
    public void run() {
            System.out.println(Thread.currentThread().getName());
    }
}

注:之前说的Callable接口那里有说过线程池,可以查看用法。

posted @ 2022-03-30 17:04  可乐加品客  阅读(66)  评论(0)    收藏  举报