面向对象基础篇 -- Java多线程编程

多线程编程

  • 进程 : 正在运行的程序,当我们的一个程序进入内存运行时,也就成了一个进程。

进程是出于运行过程中的程序,具有一定的独立功能

进程是系统进行资源分配和调度的一个独立的单位

进程是正在运行的程序,进程负责给程序分配内存

一、创建线程的几种方法

Thread<无返回值>

  • 继承Thread类
  • 重写run方法
  • 使用start方法启动线程
class Demo extends Thread{
    @Override
    public void run(){
        System.out.println("这是创建的线程任务");
    }
    public satic void main(String[] args){
        //开启线程任务
        Demo demo = new Demo();
        demo.start();
    }
}

Thread常用方法
  • Thread.currentThread().getName() 获取线程名称
  • getName() 返回线程名称
  • getId() 返回该线程的标识符
  • getPriority() 返回优先级
  • getState() 返回线程状态
  • interrupt() 中断线程
  • isInterrupted() 测试线程是否已经中断。
  • isAlive() 测试线程是否处于活动状态。
  • isDaemon() 测试该线程是否为守护线程。
  • join() 等待该线程终止。
  • setDaemon(boolean) 将该线程标记为守护线程或用户线程。
  • setName(java.lang.String) 改变线程名称,使之与参数 name 相同。
  • setPriority(int) 更改线程的优先级。
  • sleep(long) 在指定的毫秒数内让当前正在执行的线程休眠(暂停执行),此操作受到系统计时器和调度程序精度和准确性的影响。
  • yield() 暂停当前正在执行的线程对象,并执行其他线程。暗示进程放弃依次资源抢占

Runnable<无返回值>

  • 实现Runnable接口
  • 实现run方法
  • 通过new Thread(实现Runnable接口的对象).start()启动线程

实现接口

class Demo01 implement Runnable{
    public void run(){
         System.out.println("这是创建的线程任务");
    }
    public satic void main(String[] args){
        //开启线程任务
        Demo01 demo = new Demo01();
        new Thread(demo).start();
    }
}

匿名内部类

class Dmeo02{
    public static void main(String[]  args){
        new Thread(()->{
            System.out.println("这是创建的线程任务");
        }).start();
    }
}

Callable <有返回值>

通过继承Callable接口

实现call方法

class CallableDemo implements Callable<Integer>{
    @Override
    public Integer call() throws Exception{
        int sum = 0 ;
        for(int i = 0 ; i < 100 ; i ++){
            sum += i;
            System.out.println("使用Callable接口创建子线程"+i);
        }
        return sum ;
    }
}

FutureTask

我们实现Callable 之后,我们发现这个类是没有start方法 的

此时,我们就需要借助FutureTask这个类

这个类它实现了Future接口

而Future接口继承了我们的Runnable 和Callable 接口

此时,我们就可以通过如下方法,创建这个线程(多态的原理)

class Test{
    public static void main(String[] args){
        CallableDemo demo = new CallableDemo();
        FutureTask ft = new FutureTask(demo);
        new Tread(ft).start();
        
       //我们可以通过get方法来获取这个返回值
        try{
            Integer sum = (Integer)ft.get();
            System.out.println("Callable子线程返回:"+sum);
        }catch(Expection e){
            e.printStacjTrace();
        }
    }
}

二、线程安全问题

Thread 和 Runnable 的区别

  • 对于Thread
class Demo extends Thread {
    private int count ;
    
    @Override
    public void run(){
        for(int i = 0 ; i < 10 ; i ++){
            count ++ ;
        }
        System.out.println(count);
    }
    
    public static void main(String[] args){
        Demo demo = new Demo();
        Demo demo1 = new Demo();
        demo.start();
        demo1.start();
    }
}
/*执行输出:
10
10
*/
  • 对于Runnable
class Demo implements Runnable{
        private int count ;
    
    @Override
    public void run(){
        for(int i = 0 ; i < 10 ; i ++){
            count ++ ;
        }
        System.out.println(count);
    }
    
    public static void main(String[] args){
        Demo demo = new Demo();
        Demo demo1 = new Demo();
        new Thread(demo).start();
        new Thread(demo1).start();
    }
}
/*执行输出:
7
7
多次执行发现,没次结果都不大相同
*/

通过对上方的两个程序结果对比

我们发现

继承Thread 的线程对象,不能够共享数据

实现Runnable 的线程对象,使用不同的Thread对象,同时执行一个实现Runnable接口的对象,则会出现共享数据的现象,可能会出现线程安全问题.

三、如何解决线程安全问题

通过加锁,解决

synchronized关键字 – 锁

  • java提供的同步锁,synchronized有三种使用方式
  • 必须使用一个对象作为钥匙
  • 锁加的范围越小,效率越高
三种使用方法
  • 同步块
 synchronized(key){
               //同步代码,key必须是一个对象
           }
class Demo implements Runnable{
    private int count ;
    Object obj = new Object();
    
    @Override
    public void run(){
        for(int i = 0 ; i < 10 ; i ++){
           synchronized(obj){
                count ++ ;
           }
        }
        System.out.println(count);
    }
    
    public static void main(String[] args){
        Demo demo = new Demo();
        Demo demo1 = new Demo();
        new Thread(demo).start();
        new Thread(demo1).start();
    }
}
/*运行结果
10
20
*/
  • 将方法加锁

如果某个方法都是可能出现线程安全问题

则建议加在方法上

该方法在充当锁的钥匙

class Demo implements Runnable{
private int count ;
Object obj = new Object();

class Demo implements Runnable{
    private int count ;
    
    @Override
    public synchronized void run(){
        for(int i = 0 ; i < 10 ; i ++){
                count ++ ;
        }
        System.out.println(count);
    }
    
    public static void main(String[] args){
        Demo demo = new Demo();
        Demo demo1 = new Demo();
        new Thread(demo).start();
        new Thread(demo1).start();
    }
}
/*运行结果
10
20
*/
  • 将静态方法加锁

此时充当钥匙的是,静态方法所在的静态类

类.class 可以拿到当前类的字节码对象

效率

synchronized 同步锁在jdk7之前,默认调用系统锁(重量级锁)效率慢

基于如上原因,jdk5,JUC包诞生,提供了一个Lock锁

Lock接口

lock锁,是jdk5.0提供的锁,是一个可重入锁(ReentrantLock)

可以充当公平锁,也可以充当不公平锁

一旦加锁,最后必须释放锁,否则会出现死锁现象

将释放锁的代码写入finally中

常用的实现子类 ReentrantLock
class Demo implements Runnable{
    private int count ;
    private Lock lock = new ReentrantLock();
    @Override
    public  void run(){
        for(int i = 0 ; i < 10 ; i ++){
            try{
                //手动上锁
            lock.lock();
                count ++ ;
                //必须写入try中
            }finally{
                 //手动解锁
                lock.unlock();
            }
            
        }
        System.out.println(count);
    }
    
    public static void main(String[] args){
        Demo demo = new Demo();
        Demo demo1 = new Demo();
        new Thread(demo).start();
        new Thread(demo1).start();
    }
}
/*运行结果
10
20
*/

synchronized锁升级

synchronized:偏向锁(无锁),自旋锁(CAS),重量级锁
乐观锁和悲观锁:
  • synchronized 和 Lock 典型的悲观锁

死锁

两个线程之间,进行资源竞争时,彼此之间拿着对方需要的资源,但是因为线程的竞争性(请求保存),形成一种相互等待对方释放资源的现象,这种现象就叫死锁

死锁的必要条件
  • 互斥
  • 请求保存
  • 环路等待
  • 不可剥夺性

死锁只能尽量避免,不能完全解决

单例模式

我们在编程中,会遇到多个地方使用同一个对象的情况

而为了避免对象的重复创建

我们有两种单例模式: 饿汉式,懒汉式

饿汉式
  • 它是将一个已经创建好的静态对象,通过一个访问器,向外提供
class Demo{
    //已经创建好的静态对象
    private static Demo demo = new Demo();
    //私有化我们的构造函数
    private Demo(){}
    //提供一个访问器
    public Demo newDemo(){
        return this.demo;
    }
}

此时我们在外部就可以通过访问器,来访问这个已经创建好的对象

懒汉式
  • 和饿汉式一样,都是提供一个访问器,返回内部创建好的对象
  • 不同的是,饿汉式他是已经创建好的对象,而懒汉式则是在第一次调用构造函数时才创建对象
class Demo{
    //定义一个对象变量
    private static Demo demo = null;
    //私有化我们的构造函数
    private Demo(){}
    //提供一个访问器
    public Demo newDemo(){
        //判断是否是第一次访问
        if(demo==null){
            demo = new Demo();
        }
        return this.demo;
    }
}
双重检查锁
  • 对于懒汉式来说,在单线程模式下,是没有问题的,但是在多线程下他就会出现很大的问题,

  • 例如说,a通过访问器获取对象,此时demo==null,当程序正要创建对象时,b抢占了资源,此时demo依旧为空

  • 为了避免这种情况,我们可以使用双重检查锁

class Demo{
    //定义一个对象变量
    private static Demo demo = null;
    //私有化我们的构造函数
    private Demo(){}
    //提供一个访问器
    public Demo newDemo(){
        //判断是否是第一次访问
        if(demo==null){
            //加锁
             synchronized(Demo.class){
                 //双重判断
                 if(demo==null){
                     demo = new Demo();
                 }
             }
        }
        return this.demo;
    }
}

双重检查锁,并不是一定有效的

在编译过程中,代码优化可能会改变我们的代码顺序

因此,我们必须禁止代码优化

volatile关键字

  • 被volatile修饰的变量,具有线程 可见性
  • 被volatile修饰的变量,是禁止指令重排序的

因此,正确的双重检查锁的形式应该如下

class Demo{
    //定义一个对象变量
    private static  volatile Demo demo = null;
    //私有化我们的构造函数
    private Demo(){}
    //提供一个访问器
    public Demo newDemo(){
        //判断是否是第一次访问
        if(demo==null){
            //加锁
             synchronized(Demo.class){
                 //双重判断
                 if(demo==null){
                     demo = new Demo();
                 }
             }
        }
        return this.demo;
    }
}

四、线程的生命周期

在这里插入图片描述

五、同步问题(协同步调)和 唤醒机制

同步问题实现方案

  • 可以使用lock锁来实现
  • 使用同步锁配合唤醒机制

唤醒机制

  • notify 方法 唤醒处于wait状态的对象
  • notifyAll 方法 唤醒所有处于wait状态的对象
  • wait 方法 进入到等待状态

唤醒机制必须配合 同步锁

  • 必须有一个共有对象
  • 该共有对象作为同步锁的钥匙

生产者和消费者模式(同步唤醒机制的实现):

  • 一个简单的供需平衡问题

  • 生产者

package ThreadDemo;

import java.util.Random;

/**
 * @author : YWJ
 * @date : 2022/11/14 : 20:46
 */
public class Product extends Thread{
    private String[] foods ;
    private Disk disk ;
    private Random random ;

    public Product(Disk disk) {
        this.foods = new String[]{"牛肉面","兰州拉面","削筋面","担担面","重庆小面","浆水面","油泼面"};
        this.disk = disk ;
        this.random = new Random();
    }

    @Override
    public void run() {
        makeFood();
    }

    private void makeFood() {
        synchronized (this.disk) {
            for (int i = 0; i < 10; i++) {
                if (this.disk.isFull()) {
                    //创造食物
                    String food = this.foods[this.random.nextInt(this.foods.length)];
                    System.out.println("生产了一个食物:" + food);
                    this.disk.setFood(food);
                    this.disk.setFull(false);
                    //唤醒消费者
                    this.disk.notify();
                    //让生产者进入等待
                    try {
                        this.disk.wait();
                    } catch (InterruptedException e) {
                        e.printStackTrace();
                    }
                } else {
                    //让生产者进入等待
                    try {
                        this.disk.wait();
                    } catch (InterruptedException e) {
                        e.printStackTrace();
                    }
                }
            }
        }
    }
}

  • 消费者
package ThreadDemo;

/**
 * @author : YWJ
 * @date : 2022/11/14 : 20:46
 */
public class Customer extends Thread {
    private Disk disk ;


    public Customer(Disk disk,String name) {
        super(name);
        this.disk = disk;
    }


    @Override
    public void run() {
        synchronized (this.disk) {
            while (true) {
                if (!this.disk.isFull()) {
                    String food = this.disk.getFood();
                    System.out.println(currentThread().getName() + "消费了一次 :" + food);
                    this.disk.setFull(true);

                    this.disk.notify();
                    try {
                        this.disk.wait();
                    } catch (InterruptedException e) {
                        e.printStackTrace();
                    }
                }else {
                    try {
                        this.disk.wait();
                    } catch (InterruptedException e) {
                        e.printStackTrace();
                    }
                }
            }
        }
    }
}

  • 共有属性盘子
package ThreadDemo;

/**
 * @author : YWJ
 * @date : 2022/11/14 : 20:44
 */
public class Disk {

    public String getFood() {
        return food;
    }

    public void setFood(String food) {
        this.food = food;
    }

    public boolean isFull() {
        return full;
    }

    public void setFull(boolean full) {
        this.full = full;
    }

    private String food;
    private boolean full = true;

}
  • 代码测试
package ThreadDemo;

/**
 * @author : YWJ
 * @date : 2022/11/14 : 21:14
 */
public class TestCP {
    public static void main(String[] args) {
        Disk disk = new Disk();
        Product  product = new Product(disk);
        Customer customer = new Customer(disk,"张三");
        //将消费者设置成守护线程
        customer.setDaemon(true);
        product.start();
        customer.start();
    }
}

  • 执行结果

生产了一个食物:削筋面
张三消费了一次 :削筋面
生产了一个食物:重庆小面
张三消费了一次 :重庆小面
生产了一个食物:削筋面
张三消费了一次 :削筋面
生产了一个食物:削筋面
张三消费了一次 :削筋面
生产了一个食物:油泼面
张三消费了一次 :油泼面
生产了一个食物:担担面
张三消费了一次 :担担面
生产了一个食物:担担面
张三消费了一次 :担担面
生产了一个食物:削筋面
张三消费了一次 :削筋面
生产了一个食物:削筋面
张三消费了一次 :削筋面
生产了一个食物:担担面
张三消费了一次 :担担面

  • 0
    点赞
  • 0
    收藏
    觉得还不错? 一键收藏
  • 0
    评论
评论
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

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

抵扣说明:

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

余额充值