Java中多线程的相关知识

本文详细介绍了Java中创建线程的三种方式:继承Thread类、实现Runnable接口和实现Callable接口,并探讨了线程的生命周期,包括新建、就绪、运行、阻塞和消亡状态。此外,还讲解了线程的常用方法,如设置优先级、join、sleep和设置守护线程。线程同步方面,通过同步代码块、同步方法和Lock锁进行了深入分析,同时讨论了线程同步的优缺点。最后,通过生产者-消费者模型展示了线程通信的wait、notify方法以及Lock和Condition的使用,强调了线程安全和效率的重要性。

一、创建线程的三种方式

1、继承Thread类

新建一个类,当这个类继承Thread类,同时重写Thread类中的run方法,并且线程的任务/逻辑写在run方法中,这个类才具备多线程能力。

注:(1)mian中创建具体的线程对象,想要执行线程中的任务,不能直接调用run方法,需要用Thread类中的start方法。
  (2)线程对象创建并执行需要写在主线程欲执行代码之前。

代码示例:
以主函数和线程类分别需要打印十个数为例。

public class ThreadCreate extends Thread{
    @Override
    public void run() {
        for(int i = 0; i < 10; i++){
            System.out.println("Thread:"+i);
        }
    }
}
public class Test {
    public static void main(String[] args) {
        //创建一个具体的线程对象
        ThreadCreate tc = new ThreadCreate();
        //tc.run();  //不能直接调用run方法,直接调用会被当成是一个普通方法
        tc.start();  //start()是Thread类中的方法

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

运行结果:
在这里插入图片描述


补充: 设置并读取线程名字

方式(1):setName(); getName();方法
主函数中使用Thread.currentThread().setName(“主线程”); 设置主函数的名字,其中Thread.currentThread():用于获取当前正在执行的线程
主函数中使用线程对象名.setName(“子线程”); 设置子线程名字,并在子线程中使用this.getName()获取当前子线程的名字。
代码示例:

Thread.currentThread().setName("主线程");
System.out.println(Thread.currentThread().getName()+i);
tc.setName("子线程");
System.out.println(this.getName()+i);

方式(2):通过构造器设置名字
子线程设置名字不使用setName();方法,转而在子线程中添加一个带参构造器。

	public ThreadCreate(String name){
        super(name);//调用父类的有参构造器
    }

2、实现runnable接口

代码示例:

public class ThreadCreate2 implements Runnable{

    @Override
    public void run() {
        for(int i = 0; i < 100; i++){
            System.out.println(Thread.currentThread().getName()+i);
        }
    }
}
public class Test {
    public static void main(String[] args) {
    	//创建子线程对象
        ThreadCreate2 tc2 = new ThreadCreate2();
        Thread t1 = new Thread(tc2, "子线程1");
        t1.start();
		
		Thread t2 = new Thread(tc2, "子线程2");
        t2.start();
	
		Thread t3 = new Thread(tc2, "子线程3");
        t3.start();

        for(int i = 0; i < 100; i++)
            System.out.println(Thread.currentThread().getName()+i);
    }

注:
1、在实际开发中,使用方式2(实现Runnable接口)的方式更多
   原因:(1)方式1会有Java单继承的局限性,如果继承了Thread类,就不能在继承其他的类
      (2)方式2的共享资源的能力会更强
方式1在进行多线程共享资源时,可能会出现不同线程抢占同一资源,出现错误情况,即线程不安全,而方式2避免了这一情况。
2、Thread类实际也是Runnable接口的一个实现类


3、实现Callable接口

对比前两种创建线程的方式,都需使用run()方法。而run方法有局限性:没有返回值、不能抛出异常。为了弥补不足,提出第三种方式:实现Callable接口,但实现比较复杂。

1、实现Callable接口,可以不带泛型,如果不带泛型,默认返回object类型
2、如果带泛型,则返回泛型对应的类型
3、call方法有返回值,可以抛出异常

代码示例:

public class ThreadCreate3 implements Callable<Integer> {

    @Override
    public Integer call() throws Exception {
        return new Random().nextInt(100);
    }
}

class Test01{
    public static void main(String[] args) throws ExecutionException, InterruptedException {
        ThreadCreate3 tc3 = new ThreadCreate3();
        FutureTask ft = new FutureTask(tc3);
        Thread t = new Thread(ft);
        t.start();
        Object obj = ft.get();
        System.out.println(obj);
    }
}

二、线程的生命周期

在线程的生命周期中,包括新建、就绪、运行、阻塞和消亡(死亡)五个状态。

新建状态: 当程序使用new创建一个线程,jvm为其分配内存并初始化,即为新建状态;
就绪状态: 当线程对象调用start()方法,就进入就绪状态,此时该线程有资格被运行,但还没获取到cpu的执行权;
运行状态: 处于就绪状态的线程获取cpu执行权,调用run()方法开始执行线程;
消亡状态: 当线程执行完毕,进入消亡状态。但有以下几种进入消亡状态的情况:(1)程序正常执行结束;(2)程序出现异常;(3)主动调用stop方法终止该线程(被弃用);
阻塞状态: 线程在运行状态出现阻塞事件会进入阻塞状态,当阻塞事件处理完毕,程序重新进入就绪状态。

图示:
在这里插入图片描述


三、线程常用方法

先前使用过的方法:

1、start():启动当前线程,表面上调用start方法,实际是调用线程里的run方法;
2、run():线程类继承Thread类或者实现Runnable接口的时候,都要重新实现这个run方法,run方法里面就是线程要执行的内容;
3、currentThread():Thread类中的一个静态方法:获取当前正在执行的线程;
4、setName():设置线程名字;
5、getName():读取线程名字;

新的方法:
1、设置优先级
对于同优先级别的线程,采取的策略是先到先服务,使用时间便轮转策略;对于不同优先级的,优先级别高的则被cpu调度的概率就大,但不代表一定优先调度。

方式:t.setPriority(10);

注意:必须先setPriority,再start!
2、join方法
当一个线程调用了join方法,这个线程会被先执行,它执行结束之后才可以执行别的线程。

方式:t.start();
   t.join();

注意:必须先start,再join!
3、sleep方法
人为的制造阻塞事件

方式:Thread.sleep(1000);

4、setDaemon方法
设置伴随线程:将子线程设置为主线程的伴随线程,主线程停止的时候,子线程也不继续执行。

方式:t.setDaemon(true);

注意:必须先setDaemon,再join!
5、stop方法


四、线程同步

引入:多个线程,在争抢资源的过程中,导致共享的资源出现问题。一个线程还没执行完,另一个线程就参与进来,开始争抢资源。
解决方案:加“锁”(同步/同步监视器)

方式1、同步代码块

方式:synchronized (类.class){
   …
   }

注:必须保证多个线程用的是同一把锁!!!
代码示例(以抢票为例):

public class BuyTicketThread extends Thread{
    public BuyTicketThread(String name){
        super(name);
    }
    static int ticketNum = 10;
    @Override
    public void run() {
        for(int i = 1; i <= 100; i++){
        	//把具有安全隐患的代码锁住即可,如果锁多了会降低效率
        	//this指的是这个锁
            //synchronized (this){
            //使用BuyTicketThread.class(当前类对应的字节码信息)当锁
            synchronized (BuyTicketThread.class){
            if(ticketNum > 0)
                System.out.println("我在"+this.getName()+"买到了第"+ticketNum--+"张票");
            }
        }
    }
}
public class BuyTicketMain {
    public static void main(String[] args) {
        BuyTicketThread btt1 = new BuyTicketThread("窗口1");
        btt1.start();
        BuyTicketThread btt2 = new BuyTicketThread("窗口2");
        btt2.start();
        BuyTicketThread btt3 = new BuyTicketThread("窗口3");
        btt3.start();
    }
}

同步监视器总结:
1、同步监视器必须是引用数据类型,不能是基本数据类型;
2、可以创建一个专门的同步监视器,没有任何业务含义;
3、一般使用共享资源做同步监视器即可;
4、在同步代码块中不能改变同步监视器对象的引用;
5、尽量不要使用String和Integer类做同步监视器;
6、建议使用final修饰同步监视器。

同步代码块的执行过程:
1、线程1来到同步代码块,发现同步监视器为open状态,需要close,然后执行其中的代码;
2、线程1在执行过程中,发生了线程切换(阻塞、就绪),线程1失去了cpu,但没有开锁open;
3、线程2获取了cpu,来到了同步代码块,发现同步监视器为close状态,无法执行其中的代码,线程2进入阻塞状态;
4、线程1再次获取cpu,接着执行后续代码,同步代码块执行完毕,释放锁open;
5、线程2也再次获取cpu,来到了同步代码块,发现同步监视器open状态,拿到锁并且上锁,由阻塞状态进入就绪状态,再进入运行状态,重复线程1的处理过程(加锁);
同步代码块中能发生cpu的切换,但后续被执行的线程也无法执行同步代码块!(因为锁仍处于close状态)

其他:
1、多个代码块使用同一个同步监视器(锁),锁住一个代码块的同时,也锁住使用该锁的所有代码块,其他线程无法访问其中的任何一个代码块;
2、多个代码块使用同一个同步监视器(锁),锁住一个代码块的同时,也锁住使用该锁的所有代码块,但没有锁住使用其他同步监视器的代码块,其他线程有机会访问其他同步监视器的代码块。


方式2、同步方法

把可能会出现安全隐患的代码块封装成一个方法,该方法用static和synchronized修饰。
代码示例:

public class BuyTicketThread extends Thread{
    public BuyTicketThread(String name){
        super(name);
    }
    static int ticketNum = 10;
    @Override
    public void run() {
        for(int i = 1; i <= 100; i++){
//            synchronized (this){ }
            buyTicket();
        }
    }

    public static synchronized void buyTicket(){
        if(ticketNum > 0)
            System.out.println("我在"+Thread.currentThread().getName()+"买到了第"+ticketNum--+"张票");
    }
}
public class BuyTicketMain {
    public static void main(String[] args) {
        BuyTicketThread btt1 = new BuyTicketThread("窗口1");
        btt1.start();
        BuyTicketThread btt2 = new BuyTicketThread("窗口2");
        btt2.start();
        BuyTicketThread btt3 = new BuyTicketThread("窗口3");
        btt3.start();

    }
}

总结:
1、不要将run()方法定义为同步方法,因为这样效率太低;
2、非静态同步方法的同步监视器是this,静态同步方法的同步监视器是类名.class 字节码信息对象;
3、同步代码块的效率要高于同步方法,因为同步方法是将线程挡在了方法的外部,而同步代码块将线程挡在了代码块的外部,但却是方法的内部;
4、同步方法的锁是this,一旦锁住一个方法,就锁住了所有的同步方法,同步代码块只是锁住使用该同步监视器的代码块,而没有锁住使用其他监视器的代码块。


方式3、Lock锁

与synchronized相比,lock锁可提供多种锁方案,更灵活。
代码示例:

public class BuyTicketThread2 implements Runnable{

    //创建一把锁
    Lock lock = new ReentrantLock();    //多态(接口=实现类),可以使用不同的实现类

    public static int ticketNum = 10;

    @Override
    public void run() {
        for(int i = 1; i <= 100; i++){
            //打开锁
            lock.lock();
            try {
                if(ticketNum > 0)
                    System.out.println("我在"+Thread.currentThread().getName()+"买到了第"+ticketNum--+"张票");
            }catch (Exception e){
                e.printStackTrace();
            }finally {
                //关闭锁
                lock.unlock();
            }
        }
    }
}
public class BuyTicketMain2 {
    public static void main(String[] args) {
        BuyTicketThread2 btt = new BuyTicketThread2();
        Thread t1 = new Thread(btt, "窗口1");
        Thread t2 = new Thread(btt, "窗口2");
        Thread t3 = new Thread(btt, "窗口3");
        t1.start();
        t2.start();
        t3.start();
    }
}

synchronized是Java中的关键字,这个关键字的识别是靠JVM来识别完成的,是虚拟机级别的;而Lock锁是API级别的,提供了相应的接口和对应的实现类,这个方式更灵活,表现出来的性能更优越。

Lock和synchronized的区别:
1、Lock是显示锁(手动开启和关闭锁),synchronized是隐式锁;
2、Lock只有代码块锁,synchronized有代码块锁和方法锁;
3、使用Lock锁,JVM将话费较少的时间来调度线程,性能更好,并有更好的扩展性(提供更多的子类)。

优先使用顺序
Lock > 同步代码块(进入方法体,分配了相应资源) > 同步方法(方法体之外)


4、线程同步的优缺点

线程安全:效率低,可能带来死锁
线程不安全:效率高


五、线程通信

以生产者-消费者为例,假设只有一个空间,生产者生产完一个商品,消费者才能获取该商品,消费者消费完,生产者再生产,如此循环。

1、未加线程同步之前

代码示例:
Product(商品类):

public class Product {
    private String brand;
    private String name;

    public String getBrand() {
        return brand;
    }

    public void setBrand(String brand) {
        this.brand = brand;
    }

    public String getName() {
        return name;
    }

    public void setName(String name) {
        this.name = name;
    }
}

ProducerThread(生产者线程):

public class ProducerThread extends Thread{
    private Product p;

    public ProducerThread(Product p) {
        this.p = p;
    }

    @Override
    public void run() {
        for(int i = 1; i <= 10; i++){
            if(i % 2 ==0){
                p.setBrand("费列罗");
                try {
                    Thread.sleep(100);
                } catch (InterruptedException e) {
                    e.printStackTrace();
                }
                p.setName("巧克力");
            }else {
                p.setBrand("苹果");
                try {
                    Thread.sleep(100);
                } catch (InterruptedException e) {
                    e.printStackTrace();
                }
                p.setName("电脑");
            }
            System.out.println("生产者生产了"+p.getBrand()+"的"+p.getName());
        }
    }
}

ConsumerThread(消费者线程):

public class ConsumerThread extends Thread{
    private Product p;

    public ConsumerThread(Product p) {
        this.p = p;
    }

    @Override
    public void run() {
        for(int i = 1; i <= 10; i++){
            System.out.println("消费者消费了"+p.getBrand()+"的"+p.getName());
        }
    }
}

Main函数:

public class MainTest {
    public static void main(String[] args) {
        Product p = new Product();
        ProducerThread pt = new ProducerThread(p);
        ConsumerThread ct = new ConsumerThread(p);
        pt.start();
        ct.start();
    }
}

未加线程同步之前,两个线程会出现争抢资源的情况,导致结果出现混乱。如下:
在这里插入图片描述


2、加线程同步后

为了解决上述问题,需要采用线程同步。

(1)同步代码块的方式

修改后的ProducerThread(生产者线程)部分代码:

@Override
    public void run() {
        for(int i = 1; i <= 10; i++) {
            synchronized (p) {
                if (i % 2 == 0) {
                    p.setBrand("费列罗");
                    try {
                        Thread.sleep(100);
                    } catch (InterruptedException e) {
                        e.printStackTrace();
                    }
                    p.setName("巧克力");
                } else {
                    p.setBrand("苹果");
                    try {
                        Thread.sleep(100);
                    } catch (InterruptedException e) {
                        e.printStackTrace();
                    }
                    p.setName("电脑");
                }
                System.out.println("生产者生产了" + p.getBrand() + "的" + p.getName());
            }
        }
    }

修改后的ConsumerThread(消费者线程)部分代码:

@Override
    public void run() {
        for(int i = 1; i <= 10; i++){
            synchronized (p){
                System.out.println("消费者消费了"+p.getBrand()+"的"+p.getName());
            }
        }
    }

(2)同步方法的方式

修改后的Product(商品类)部分代码:

	public synchronized void setProduct(String brand, String name){
        this.setBrand(brand);
        try {
            Thread.sleep(100);
        } catch (InterruptedException e) {
            e.printStackTrace();
        }
        this.setName(name);
        System.out.println("生产者生产了" + this.getBrand() + "的" + this.getName());
    }

    public synchronized void getProduct(){
        System.out.println("消费者消费了"+this.getBrand()+"的"+this.getName());
    }

修改后的ProducerThread(生产者线程)部分代码:

	@Override
    public void run() {
        for(int i = 1; i <= 10; i++) {
            if (i % 2 == 0) {
                p.setProduct("费列罗", "巧克力");
            } else {
                p.setProduct("苹果", "电脑");
            }
        }
    }

修改后的ConsumerThread(消费者线程)部分代码:

	@Override
    public void run() {
        for(int i = 1; i <= 10; i++){
            p.getProduct();
        }
    }

3、wait()和notify()方法

上述代码可以解决线程同步问题,但会有生产者生产多个商品,消费者消费多个商品的情况,为了解决这一问题,使得生产者和消费者交替执行,引入wait()和notify()方法

方法:
  给product类加一个布尔类型的变量flag,用来标记是否有商品(true表示有商品,false表示没有商品);
  分别在生产者和消费者同步方法中进行判断,决定是否生产/消费,是否wait()或nofity()。

修改后的Product(商品类)部分代码:

	//true:有商品;false:没有商品
    boolean flag = false;//默认情况下,没有商品,先生产再消费
    
	public synchronized void setProduct(String brand, String name){
        if(flag == true){   //如果为true,表示有商品,则生产者不生产,等待消费者消费
            try {
                wait();
            } catch (InterruptedException e) {
                e.printStackTrace();
            }
        }
        this.setBrand(brand);
        try {
            Thread.sleep(100);
        } catch (InterruptedException e) {
            e.printStackTrace();
        }
        this.setName(name);
        System.out.println("生产者生产了" + this.getBrand() + "的" + this.getName());

        //生产完,flag变成true,同时通知消费者消费
        flag = true;
        notify();
    }

    public synchronized void getProduct(){
        if(flag == false){  //如果为false,表示没有商品,消费者不消费,等待生产者生产
            try {
                wait();
            } catch (InterruptedException e) {
                e.printStackTrace();
            }
        }
        System.out.println("消费者消费了"+this.getBrand()+"的"+this.getName());

        //消费完,flag变成false,同时通知生产者生产
        flag = false;
        notify();
    }

原理:在Java对象中,有两种池:锁池(synchronized)、等待池(wait(); notify(); notifyAll();)
   如果一个线程调用了某个对象的wait方法,那么该线程进入到该对象的等待池中(并且已经将锁释放);如果未来的某一时刻,另一个线程调用了相同对象的notify方法或者notifyAll方法,那么该等待池中的线程就会被唤醒,然乎进入到对象的锁池里面去获得该对象的锁;如果获得锁成功,那么该线程就会沿着wait方法之后的路径继续执行,注意是沿着wait方法之后。(在代码中的体现就是只有if判断,没有else)

注意:
1、wait和notify方法必须放在同步方法或者同步代码块中才能生效(因为线程的通信只有在同步的基础上才有效);
2、sleep方法和wait方法的区别:sleep方法进入阻塞状态但没有释放锁,而wait进入阻塞状态同时释放了锁。


4、进一步优化

上述代码解决生产者-消费者问题,仍存在问题,即当有多个生产者,多个消费者时,一个生产者/消费者线程使用wait方法,其余的线程均进入等待队列,使用notify唤醒时,所有均被唤醒,这时生产者线程仍可能唤醒生产者线程,消费者线程仍可能唤醒消费者线程。出现这种问题,需要使用Lock锁和Condition进行解决。
修改后的Product(商品类)部分代码:

	//Lock锁
    Lock lock = new ReentrantLock();
    //生产者等待队列
    Condition produceCondition = lock.newCondition();
    //消费者等待队列
    Condition consumeCondition = lock.newCondition();

	public void setProduct(String brand, String name){
        lock.lock();
        try {
            if(flag == true){   //如果为true,表示有商品,则生产者不生产,等待消费者消费
                try {
                    //wait();
                    produceCondition.await();   //生产者阻塞,生产者进入等待等列
                } catch (InterruptedException e) {
                    e.printStackTrace();
                }
            }
            this.setBrand(brand);
            try {
                Thread.sleep(100);
            } catch (InterruptedException e) {
                e.printStackTrace();
            }
            this.setName(name);
            System.out.println(Thread.currentThread().getName()+":生产者生产了" + this.getBrand() + "的" + this.getName());

            //生产完,flag变成true,同时通知消费者消费
            flag = true;
            //notify();
            consumeCondition.signal();
        }catch (Exception e){
            e.printStackTrace();
        }finally {
            lock.unlock();
        }
    }

    public void getProduct(){
        lock.lock();
        try {
            if(flag == false){  //如果为false,表示没有商品,消费者不消费,等待生产者生产
                try {
                    //wait();
                    consumeCondition.await();   //消费者阻塞,消费者进入等待队列
                } catch (InterruptedException e) {
                    e.printStackTrace();
                }
            }
            System.out.println(Thread.currentThread().getName()+":消费者消费了"+this.getBrand()+"的"+this.getName());

            //消费完,flag变成false,同时通知生产者生产
            flag = false;
            //notify();
            produceCondition.signal();
        }catch (Exception e){
            e.printStackTrace();
        }finally {
            lock.unlock();
        }
    }

Main函数:

public class MainTest {
    public static void main(String[] args) {
        Product p = new Product();
        ProducerThread pt = new ProducerThread(p);
        ConsumerThread ct = new ConsumerThread(p);

        Thread pt1 = new Thread(pt, "生产者线程1");
        Thread pt2 = new Thread(pt, "生产者线程2");
        Thread pt3 = new Thread(pt, "生产者线程3");

        Thread ct1 = new Thread(ct, "消费者线程1");
        Thread ct2 = new Thread(ct, "消费者线程2");
        Thread ct3 = new Thread(ct, "消费者线程3");

        pt1.start();
        pt2.start();
        pt3.start();

        ct1.start();
        ct2.start();
        ct3.start();
    }
}

运行结果:
在这里插入图片描述

Condition:
一种用来替代传统的Object的wait()、notify()实现线程间协作的方式
一个Condition包含一个等待队列,一个Lock可以产生多个Condition,所以可以有多个等待队列;
在Object的监视器模型上,一个对象拥有一个同步队列和等待队列,而Lock(同步器)拥有一个同步队列和多个等待队列;
Object中的wait()、notify()、notifyAll()方法是和“同步锁”(synchronized关键字)捆绑使用,而Condition是与“互斥锁/共享锁”捆绑使用,就是说必须在lock.lock()和lock.unlock()之间才能使用。

评论
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

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

抵扣说明:

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

余额充值