Java之多线程

进程与线程

  • 进程

    进程是程序的一次动态执行过程,它需要经历从代码加载,代码执行到执行完毕的一个完整的过程,这个过程也是进程本身从产生,发展到最终消亡的过程。多进程操作系统能同时达运行多个进程(程序),由于 CPU 具备分时机制,所以每个进程都能【循环】获得自己的CPU 时间片。由于 CPU 执行速度非常快,使得所有程序好像是在同时运行一样

  • 线程

    线程是比进程更小的执行单位,线程是进程的基础之上进行进一步的划分。所谓多线程是指一个进程在执行过程中可以产生多个更小的程序单元,这些更小的单元称为线程,这些线程可以同时存在,同时运行,一个进程可能包含多个同时执行的线程

    比如两个处理文件的程序A、B同时在执行,CPU会循环给它们俩时间片,A执行一会,再换B;A是一个多线程程序,分两个线程,分别从文件开头和文件结尾处理,B则是单线程;最终A会以快于B一倍的速度执行完

多线程的作用

更充分的利用CPU的资源,以提高程序运行的效率

PS:只有多核CPU才能实现真正的多线程,一个CPU(核)在同一时间,只能分配给一个线程

线程的创建

继承Thread类

继承Thread类,重写run方法,在main方法(主线程)中调用该类对象 的start方法,即可实现多线程同时执行

public class MyThread extends Thread{
    @Override
    public void run() {
        for (int i = 0; i < 20; i++) {
            System.out.println("This is my thread " + i);
        }
    }

    public static void main(String[] args) {
        MyThread myThread = new MyThread();
        myThread.start();

        for (int i = 0; i < 20; i++) {
            System.out.println("This is main thread " + i);
        }
    }
}
实现Runnable接口

实现Runnable接口,重写run方法;调用Thread类的构造方法,并以实现Runnable接口的类的对象作为参数;调用Thread类对象的start方法

public class MyRunnable implements Runnable {
    @Override
    public void run() {
        for (int i = 0; i < 20; i++) {
            System.out.println("This is my thread " + i);
        }
    }

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

        new Thread(myRunnable).start();

        for (int i = 0; i < 20; i++) {
            System.out.println("This is main thread " + i);
        }
        
        //lambda表达式写法
        Runnable runnable = ()->{
            for (int i = 0; i < 20; i++) {
                System.out.println("This is my thread " + i);
            }
        };
        new Thread(runnable).start();
    }
}

PS:此方式底层是通过【静态代理模式】实现的

PS:Lambda表达式,一种语法糖,用于简化函数式接口的实现

public class TestLambda {
    public static void main(String[] args) {
        //普通调用
        ILike like = new Like();
        like.lambda();

        //匿名内部类调用
        like = new ILike() {
            @Override
            public void lambda() {
                System.out.println("normal use");
            }
        };
        like.lambda();


        //lambda简化
        like = ()->{
            System.out.println("use lambda is good");
        };
        like.lambda();

        //有参数时
        ILove iLove = (int a)->{
            System.out.println(a);
        };
        iLove.lambda(1);

        //简化类型
        iLove = (a)->{
            System.out.println(a);
        };

        //简化括号 参数=1
        iLove = a->{
            System.out.println(a);
        };

        //简化花括号 方法体=1行
        iLove = a-> System.out.println(a);
    }
}

interface ILike{
    void lambda();
}

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

interface ILove{
    void lambda(int a);
}
实现Callable接口

实现Callable接口,重写call方法,call方法必须有一个返回值,并且与Callable声明的泛型类型一致

public class MyCallable implements Callable<Boolean> {
    @Override
    public Boolean call() {
        for (int i = 0; i < 20; i++) {
            System.out.println("This is my thread " + i);
        }
        return true;
    }

    public static void main(String[] args) throws ExecutionException, InterruptedException {
        MyCallable myCallable = new MyCallable();

        //创建执行服务
        ExecutorService ser = Executors.newFixedThreadPool(1);
        //提交执行
        Future<Boolean> future = ser.submit(myCallable);

        //获取返回结果
        Boolean b = future.get();

        System.out.println("myCallable result is " + b);
    
        ser.shutdown();
    }
}

线程状态

线程状态

线程停止

run方法执行完,执行它的线程就会自己停止;若想让线程被手动停止,推荐使用标志位法,如下:

public class TestStop implements Runnable {
    //1、定义标志位
    private boolean flag = true;

    @Override
    public void run() {
        //2、方法体使用该标识
        while (flag){
            System.out.println("run thread");
        }
    }

    //3、对外提供方法改变标识
    public void stop(){
        this.flag = false;
        System.out.println("stop this thread");
    }

    public static void main(String[] args) throws InterruptedException {
        TestStop testStop = new TestStop();
        new Thread(testStop).start();
        //主线程暂停2s
        Thread.sleep(2000);
        testStop.stop();
    }
}

PS:线程终止后(进入死亡状态)就不能再次通过start方法重启了,即线程只能启动一次

线程休眠

线程休眠通过Thread类的静态方法sleep实现

  • sleep(xx)指定当前线程阻塞的毫秒数

    Thread.sleep()出现在方法体中,当前线程指的是正在调用该方法的线程

  • sleep存在InterruptedException

  • sleep时间达到后线程进入就绪状态

  • sleep可以模拟网络延迟,倒计时等

  • sleep不会释放锁

线程礼让

通过Thread类的静态方法yield实现

  • 将线程从运行状态转为可运行状态(就绪状态)
  • 能够让当前正在执行的线程暂停,但不阻塞
  • 礼让后,CPU会重新调度,该线程可能又被选中,故可能出现“礼让失败”的状况
public class TestYield {
    public static void main(String[] args) {
        MyYield myYield = new MyYield();

        for (int i = 0; i < 10; i++) {
            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() + "线程停止执行");
    }
}
线程合并

通过Thread类的静态方法join实现

下面程序执行的效果是:

一开始主线程与joinThread线程同步执行,当主线程循环到200并执行joinThread.join()后,控制台只有joinThread线程在输出,直到joinThread输出完,主线程再输出后面的内容;即执行joinThread.join()后,主线程进入了阻塞状态

public class TestJoin implements Runnable {
    @Override
    public void run() {
        for (int i = 0; i < 1000; i++) {
            System.out.println("join thread " + i);
        }
    }

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

        for (int i = 0; i < 500; i++) {
            if(i == 200){
                joinThread.join();
            }
            System.out.println("main thread" + i);
        }
    }
}
查看线程状态

通过Thread类的实例方法getState获取

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 over");
        });

        Thread.State state = thread.getState();
        System.out.println(state);

        thread.start();
        state = thread.getState();
        System.out.println(state);

        //线程不终止 打印状态
        while (state != Thread.State.TERMINATED){
            Thread.sleep(100);
            state = thread.getState();
            System.out.println(state);
        }
    }
}
线程优先级
  • JVM会用一个线程调度器来监控程序中所有进入就绪状态的线程,线程调度器按照优先级决定应该调度哪个线程来执行

  • 线程的优先级用数字表示,范围从1~10,Thread类还有如下常量可用

    Thread.MIN_PRIORITY = 1;

    Thread.NORM_PRIORITY = 5;

    Thread.MAX_PRIORITY = 10;

  • 通常使用以下方式改变或获取优先级

    getPriority().setPriority(int xxx)

public class TestPriority extends Thread {
    public static void main(String[] args) {
        System.out.println(Thread.currentThread().getName() + "-->" + Thread.currentThread().getPriority());

        MyPriority myPriority = new MyPriority();

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

        //先设置优先级
        t1.setPriority(1);
        t2.setPriority(3);
        t3.setPriority(Thread.MAX_PRIORITY);

        //再启动线程
        t1.start();
        t2.start();
        t3.start();
    }
}

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

如上代码,执行后“Thread-X–10”【通常】先被输出

PS:优先级低只是意味着获得调度的概率低

守护线程
  • JVM中把线程分为用户线程守护线程
  • 常见的守护线程有:垃圾回收线程、内存监控线程、日志记录线程等
  • JVM必须确保用户线程执行完毕,但不用等待守护线程执行完毕
  • 代码中可以通过Thread类的实例方法setDaemon(true),将线程设置为守护线程,默认是false,表示用户线程

线程同步

线程同步是解多并发问题的手段

并发问题

当多个线程操作同一个资源的情况下,会造成数据的紊乱,如买票问题

public class Ticket implements Runnable {

    private int ticketNums = 10;

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

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

        new Thread(ticket, "sam").start();
        new Thread(ticket, "mike").start();
        new Thread(ticket, "waston").start();
    }
}

运行上述代码,会出现一张票被多个人买或少卖票的情况

原因:若线程1的System.out.println卡在线程2的ticketNums–和System.out.println的中间执行,导致一张票被卖了两次;同时,也会有一张票“消失”

PS:ArrayList线程不安全的证明

public class TestList {
    public static void main(String[] args) {
        List<String> list = new ArrayList<>();
        for (int i = 0; i < 10000; i++) {
            new Thread(()->{
                list.add(Thread.currentThread().getName());
            }).start();
        }
        try {
            Thread.sleep(3000);
        } catch (InterruptedException e) {
            e.printStackTrace();
        }
        System.out.println(list.size());
        //输出结果可能不等于10000
    }
}
Synchronized关键字

上述问题展示了一种并发问题,即同一个资源,多个线程都抢着用的问题,而解决这种问题的方法,就是“排队”,一个一个来

线程同步类似于一种等待机制,让多个需要同时访问此资源的线程先进入这个资源的等待池,形成队列,一个线程使用资源完毕,下一个线程再来使用

Java中可以使用synchronized关键字来实现线程同步,效果是给资源”加锁“,锁是唯一的,因为资源(对象)是唯一的

例如:一个线程执行到被synchronized修饰的代码块时,会先去获取该代码块的锁(锁是一个对象,如下面代码中的syn),必须获得这个锁,才能执行其中的代码,锁在同一时间只能被一个线程拿着,当这个线程执行完代码块的所有内容后,会释放这个锁

public class Ticket implements Runnable {

    private int ticketNums = 10;
    boolean flag = true;

    String syn = "syn";

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

    public void buy() {
        synchronized (syn){
            if (ticketNums <= 0) {
                flag = false;
                return;
            }
            ticketNums--;
            System.out.println(Thread.currentThread().getName() + "拿到了第" + ticketNums + "张票");

        }
    }

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

        new Thread(ticket, "sam").start();
        new Thread(ticket, "mike").start();
        new Thread(ticket, "waston").start();
    }
}

synchronized也可以加在实例方法上,这样的锁是对象级别的,每个要执行该方法的线程,都必须先去获取此对象的锁,之后方可执行

public class Ticket implements Runnable {

    private int ticketNums = 10;
    boolean flag = true;

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

    public synchronized void buy() {

        if (ticketNums <= 0) {
            flag = false;
            return;
        }
        ticketNums--;
        System.out.println(Thread.currentThread().getName() + "拿到了第" + ticketNums + "张票");

    }

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

        new Thread(ticket, "sam").start();
        new Thread(ticket, "mike").start();
        new Thread(ticket, "waston").start();
    }
}
死锁

多个线程各自占有一些共享资源,并且在互相等待其他线程占有的资源,从而导致这些线程停止执行的情形

实例代码:

import java.util.Date;

public class LockTest {
    public static String obj1 = "obj1";
    public static String obj2 = "obj2";
    public static void main(String[] args) {
        LockA la = new LockA();
        new Thread(la).start();
        LockB lb = new LockB();
        new Thread(lb).start();
    }
}
class LockA implements Runnable{
    public void run() {
        try {
            System.out.println(new Date().toString() + " LockA 开始执行");
            while(true){
                synchronized (LockTest.obj1) {
                    System.out.println(new Date().toString() + " LockA 锁住 obj1");
                    Thread.sleep(3000); // 此处等待是给B能锁住机会
                    synchronized (LockTest.obj2) {
                        System.out.println(new Date().toString() + " LockA 锁住 obj2");
                        Thread.sleep(60 * 1000); // 为测试,占用了就不放
                    }
                }
            }
        } catch (Exception e) {
            e.printStackTrace();
        }
    }
}
class LockB implements Runnable{
    public void run() {
        try {
            System.out.println(new Date().toString() + " LockB 开始执行");
            while(true){
                synchronized (LockTest.obj2) {
                    System.out.println(new Date().toString() + " LockB 锁住 obj2");
                    Thread.sleep(3000); // 此处等待是给A能锁住机会
                    synchronized (LockTest.obj1) {
                        System.out.println(new Date().toString() + " LockB 锁住 obj1");
                        Thread.sleep(60 * 1000); // 为测试,占用了就不放
                    }
                }
            }
        } catch (Exception e) {
            e.printStackTrace();
        }
    }
}
Lock
  • JDK5.0后,Java提供了更强大的线程同步机制,通过显示定义同步锁对象来实现同步,同步锁使用Lock对象充当

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

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

  • 使用Lock,JVM将花费较少的时间来调度线程,性能更好

    import java.util.concurrent.locks.ReentrantLock;
      
    public class Ticket implements Runnable {
      
        private int ticketNums = 10;
        boolean flag = true;
        private final ReentrantLock lock = new ReentrantLock();
      
        @Override
        public void run() {
            while (flag){
                this.buy();
            }
        }
      
        public void buy() {
            try {
                lock.lock();//加锁
                if (ticketNums <= 0) {
                    flag = false;
                    return;
                }
                ticketNums--;
                System.out.println(Thread.currentThread().getName() + "拿到了第" + ticketNums + "张票");
            } catch (Exception e) {
                e.printStackTrace();
            } finally {
                lock.unlock();//解锁
            }
        }
      
        public static void main(String[] args) {
            Ticket ticket = new Ticket();
      
            new Thread(ticket, "sam").start();
            new Thread(ticket, "mike").start();
            new Thread(ticket, "waston").start();
        }
    }
    
生产者消费者模型
  • 假设仓库中只能存放一件产品,生产者将生产出来的产品放入仓库,消费者将仓库中的产品取走
  • 如果仓库中没有产品,生产者会将产品放入仓库,否则停止生产并等待,直到仓库中的产品被消费者取走为止
  • 如果仓库中放有产品,则消费者可以将产品取走消费,否则停止消费并等待,直到仓库中再次放入产品为止

管程法

public class TestPC {
    public static void main(String[] args) {
        Container container = new Container();

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

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

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

    //生产
    @Override
    public void run() {
        for (int i = 1; i <= 100; i++) {
            container.produce(new Product(i));
            System.out.println("生产了编号为" + i + "的产品");
        }
    }
}
//消费者
class Consumer extends Thread{
    Container container;

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

    //消费
    @Override
    public void run() {
        for (int i = 1; i <= 100; i++) {
            System.out.println("消费了编号为" + container.consume().id + "的产品");
        }
    }
}

//产品
class Product{
    int id; //产品编号

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

//缓冲区
class Container{
    //产品容器
    Product[] products = new Product[10];
    int count = 0;//产品计数器

    //生产者放入产品
    public synchronized void produce(Product product){
        //容器满了 等待消费者消费
        if(count == products.length){
            //生产者等待
            try {
                this.wait();
            } catch (InterruptedException e) {
                e.printStackTrace();
            }
        }

        //没满 放入产品
        products[count] = product;
        count++;

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

    //消费者消费产品
    public synchronized Product consume(){
        //没有产品
        if(count == 0){
            //消费者等待
            try {
                this.wait();
            } catch (InterruptedException e) {
                e.printStackTrace();
            }
        }

        //有产品 消费一个
        count--;
        Product product = products[count];

        //通知生产者继续生产
        this.notifyAll();
        return product;
    }
}

核心代码是this.wait(),作用是让某个拿到锁的线程释放锁,并进入等待队列;

this.notifyAll(),通知等待队列中的线程,使其进入锁池队列,进而获取到锁,再执行同步方法

信号灯法

public class TestPC {
    public static void main(String[] args) {
        Container container = new Container();

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

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

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

    //生产
    @Override
    public void run() {
        for (int i = 1; i <= 100; i++) {
            container.produce(new Product(i));
            System.out.println("生产了编号为" + i + "的产品");
        }
    }
}

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

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

    //消费
    @Override
    public void run() {
        for (int i = 1; i <= 100; i++) {
            System.out.println("消费了编号为" + container.consume().id + "的产品");
        }
    }
}

//产品
class Product{
    int id; //产品编号

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

//缓冲区
class Container{
    //产品容器
    Product[] products = new Product[1];
    int count = 0;//产品计数器
    boolean flag = false;//信号灯

    //生产者放入产品
    public synchronized void produce(Product product){
        //信号为true 等待消费者消费
        if(flag){
            //生产者等待
            try {
                this.wait();
            } catch (InterruptedException e) {
                e.printStackTrace();
            }
        }

        //信号为false 放入产品
        products[count] = product;
        count++;

        //通知消费者消费
        this.notifyAll();
        this.flag = !this.flag;//切换信号
    }

    //消费者消费产品
    public synchronized Product consume(){
        //信号为false
        if(!flag){
            //消费者等待
            try {
                this.wait();
            } catch (InterruptedException e) {
                e.printStackTrace();
            }
        }

        //信号为true 消费一个
        count--;
        Product product = products[count];

        //通知生产者继续生产
        this.notifyAll();
        this.flag = !this.flag;//切换信号
        return product;
    }
}

线程池

  • 经常创建和销毁线程,会造成大量的资源开销,所以Java还提供了线程池来方便我们解决这一问题
  • 线程池的底层原理是:提前创建多个线程,放入线程池中,使用时直接获取,使用完再放回池中,实现了线程的重复利用
import java.util.concurrent.ExecutorService;
import java.util.concurrent.Executors;

public class TestPool {
    public static void main(String[] args) {
        //创建线程池 参数10表示池中线程数
        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() {
        for (int i = 0; i < 10; i++) {
            System.out.println(Thread.currentThread().getName() + "--->" + i);
        }
    }
}