Java多线程

目录

进程与线程

进程

线程

进程和线程的关系

实现多任务的几种方式

多线程

 一个线程的生命周期

线程的创建和启动

线程的优先级

线程的同步

死锁

wait-notify机制及生产者消费者模型

加锁的其他方式Lock接口

读写Lock

Volatile


进程与线程

进程

当一个程序被运行,就开启了一个进程。

计算机可以同时运行多个程序,比如同时运行QQ,Word,微信等,每个进程都有自己独立运行的内存空间,每个进程的内部数据和状态都是完全独立的。

线程

在进程内部有时会需要同时执行多个子任务。例如,我们在使用Word时,可以一边打字,一边拼写检查,同时还可以在后台进行打印,我们把子任务称为线程。

进程和线程的关系

一个进程可以包含一个或多个线程,但至少会有一个线程。

线程存在于进程之中,二者之间主要区别在于,每个进程都需要操作系统为其分配独立的内存空间,而同一进程中的所有线程都在同一工作空间中工作,这些线程可以共享同一块内存和系统资源。

注意:操作系统调度的最小任务单位其实不是进程而是线程。

实现多任务的几种方式

1. 多进程模式(每个进程只有一个线程)

2. 多线程模式(一个进程有多个线程)

3. 多进程+多线程模式(最复杂)

具体采用那种方式,要考虑到进程和线程的特点。

两者对比:

1. 创建进程比创建线程开销大

2. 进程间通信比线程间通信要慢,因为线程间通信就是读写同一个变量,速度很快。

3. 多进程稳定性比多线程高,因为在多进程的情况下,一个进程崩溃不会影响其他进程,而在多线程的情况下,任何一个线程崩溃都会导致整个进程崩溃。

多线程

Java语言内置了多线程支持,一个Java程序实际上是一个JVM进程,JVM进程用一个主线程来执行main方法,在main方法内部,我们有可以启动多个线程,此外,JVM还有负责垃圾回收的其他工作线程等。简单的说,main方法一执行,就开启了主线程。每个进程至少都有一个主线程。

主进程的特点:最先开始、最先结束、产生其他的子线程、子线程结束后,清理子线程占用的内存资源。

线程是一个动态执行的过程,它也有一个从产生到死亡的过程,而这个过程呢就是我们所谓的生命周期。那我们就从线程的生命周期开始学习。

 一个线程的生命周期

线程的生命周期包含5个阶段,包括:新建、就绪、运行、阻塞、死亡。

1. 新建:使用new方法,new出线程,此时仅由Java虚拟机为其分配内存,并初始化成员变量的值。--此时仅为对象

2. 就绪:调用线程的start方法后,此时线程处于等待CPU调度执行的阶段。-- 不是执行了start方法后,此线程就会立刻执行。

3. 运行:当处于就绪的线程被调度并获得CPU资源时,便进入运行状态。(当处于就绪状态的线程获得CPU,他就会执行run方法)

对于一个单核CPU而言,同时只能执行一条指令,而JVM通过快速切换线程执行指令来达到多线程的,只是这种切换速度很快,我们感知不到罢了。为了线程切换后能恢复到正确的执行位置,每个线程都有一个独立的程序计数器,各条线程之间计数器互不影响,独立存储。

4. 阻塞:线程处于运行状态时,可能因为某些原因导致运行状态的线程变成了阻塞状态。

原因:等待I/O流的输入/输出、等待网络资源(网速)、调用sleep方法、调用wait方法、其他线程执行join方法、线程优先级比较低

5. 死亡:run方法执行完成;

线程正常结束、线程抛出未被捕获的异常或错误;

直接调用线程的stop方法结束线程--该方法容易导致死锁,通 常不建议使用。

当线程进入运行状态后,一般的操作系统是采用抢占式的方式来让线程获取CPU,所以CPU需要在多条线程之间切换,于是线程状态也会多次在运行、阻塞、就绪之间切换。

举例:去饭店吃饭,准备去吃饭就是新建状态(new),饭店生意火爆要排队,排队就是就绪状态(Runnable),轮到你了,吃饭就是运行状态(Running),准备发现没有碗筷,要等待别人给你送过来,这个状态就是阻塞(Blocked),等你吃完饭出来,吃饭这个事情就结束了,就是死亡状态了。

线程的创建和启动

1. 继承Thread类,并重写run方法

public class MyThread extends Thread{
       public void run(){
           // 线程需要执行的业务操作
       }
   }
   // 启动
   MyThread mt = new MyThread();
   mt.start();

2. 实现Runnable接口,实现run方法

public class MyThread implements Runnable{
       public void run(){
           // 线程需要执行的业务操作
       }
   }
   // 启动
   MyThread mt = new Thread(new MyThread());
   mt.start();

3. 实现Callable接口,实现call方法(Callable接口是为了解决Runnable接口没有返回值缺陷而产生的。实现Callable接口可以得到线程执行完毕后的返回值。)

public class MyThread implements Callable<String>{
       public String call() throws Exception {
           // 线程需要执行的业务操作
           return null;
       }
   }
   // 启动
   FutureTask<String> ft = new FutureTask(new MyThread());
   MyThread mt = new Thread(ft);
   mt.start();

注:线程的启动,是调用start()方法。线程启动后,就会执行线程类中的run()/call()方法,完成线程业务的处理。

那么,能不能直接调用run()方法呢?

调用start()和调用run()方法,具有不同的含义:

调用start方法,会在主线程基础上,开启新的子线程。

调用run方法,只是普通方法调用。不会在主线程的基础上,开启新的子线程。

4. 线程池创建

问题:当我们的程序,需要大量通过new Thread()方法创建执行时间短的线程,会消耗很多的系统资源导致系统的响应速度变慢。

解决办法:线程池(其实就是一个容纳多个线程的容器,其中的线程可以反复使用,省去了频繁创建线程对象的操作,无需反复创建线程而消耗过多资源)

使用线程池的步骤:

1. 创建线程池对象

2. 创建Runnable/Callable 接口子类对象

3. 提交 Runnable/Callable 接口子类对象

4. 关闭线程池

// 创建一个无限大的线程池,适用于负载较轻的场景
   ExecutorService service = Executors.newCachedThreadPool();
   
   // 创建一个固定大小的线程池
   ExecutorService service = Executors.newFixedThreadPool(5);
   // 创建一个单线程的线程池
   ExecutorService service = Executors.newSingleThreadExecutor();
   
   
   service.excute(Runnable);
   Future<V> result = service.submit(Callable<V>);
   V value = result.get();

注意:创建一个线程数为n的线程池后,可以提交超出线程个数n的任务,但是只能同时执行的任务个数为n,等有线程空闲后,才会执行后续的任务。

// 创建一个固定大小的线程池,指定演示,使用于执行延时或周期性任务
   ScheduledExecutorService service = Executors.newScheduledThread(5);
   // 延迟1秒后每3秒执行一次
   service3.scheduleAtFixedRate(new Task("AtFixedRate"),1,3,TimeUnit.SECONDS);

线程的优先级

线程的优先级是指,当两个或两个以上的线程同时处于就绪状态时,优先级高的线程会优先得到执行。

// 关于优先级的方法:
   final void setPriority(int newp) 修改线程的当前优先级
   final int getPriority()   返回线程的优先级

线程的同步

程序在执行过程中,可能会发生两个或多个线程同时访问一个资源的情况。

出现这样的情况后,可能会引发一些安全性的问题。出现安全性的问题怎么解决呢?可以设置线程的同步。

什么是线程同步呢?线程同步是指,当两个或两个以上的线程同时访问同一个资源时,为了确保数据的安全,只允许同一时间点,同一个共享资源

只被一个线程进行访问。线程同步也称为线程安全。

线程同步的结果:

1. 数据安全

2. 效率低下

那怎么实现线程同步?使用同步关键字synchronized来进行标识,实现的方式有两种,同步方法和同步块

class Account{
    /** 账户余额 */
    private volatile int money = 31000;
    // 同步方法
    public synchronized boolean getMoney(int nums){
            if(nums < money){
                this.money -= nums;
                System.out.println("取钱成功,取款金额" + nums + ",余额" + this.money);
                try {
                    Thread.sleep(20);
                } catch (InterruptedException e) {
                    e.printStackTrace();
                }
                return true;
            }else {
                System.out.println("取款失败,余额不足");
                return false;
            }
    }
    
    // 同步块
    public  boolean getMoney1(int nums){
            synchronized (this){
                if(nums < money){
                    this.money -= nums;
                    System.out.println("取钱成功,取款金额" + nums + ",余额" + this.money);
                    try {
                        Thread.sleep(20);
                    } catch (InterruptedException e) {
                        e.printStackTrace();
                    }
                    return true;
                }else {
                    System.out.println("取款失败,余额不足");
                    return false;
                }
            }
    }
}

class Get implements Runnable{
    private Account account;
    private int nums;
    public Get(Account account,int nums){
        this.account = account;
        this.nums = nums;
    }

    @Override
    public void run() {
        while (account.getMoney(nums)){
        }
    }
}

 

死锁

死锁是指在多线程情况下,多个线程同步竞争相互依赖的资源,从而造成多线程无法继续执行的情况。死锁很少发生,但一旦发生就很难调试。

举例子:当我们在乡间小路驾车行驶时,如果遇到对向来车,然后自己和对方互不相让,我们此时就是在同步竞争相互依赖的资源。

public class Die {
    public static void main(String[] args) {
        Obj a = new Obj();
        Obj b = new Obj();

        new Thread(new Runnable() {
            @Override
            public void run() {
                synchronized (a){
                    try {
                        Thread.sleep(300);
                    } catch (InterruptedException e) {
                        throw new RuntimeException(e);
                    }
                    b.test();
                }
            }
        }).start();

        new Thread(new Runnable() {
            @Override
            public void run() {
                synchronized (b){
                    try {
                        Thread.sleep(300);
                    } catch (InterruptedException e) {
                        throw new RuntimeException(e);
                    }
                    a.test();
                }
            }
        }).start();
    }
}

class Obj{
    public synchronized void test(){
        System.out.println("方法调用");
    }
}

wait-notify机制及生产者消费者模型

多线程机制,使得我们可以将需要完成的任务分为多个逻辑单元,并交给不同的线程完成。在线程同步的情况下,当一个线程在访问一个共享资源时,别的线程只能针对该资源进行等待访问。并且为避免死锁的情况出现,应当一个线程在访问完毕共享资源时,通知别的线程可以对共享资源进行访问。

问题来了,如何通知呢?

Java提供了wait-notify机制,完成线程间的通信。

1. Java使用wait()、notify()和notifyAll()方法,完成线程间的通信。这些方法是作为 Object 类中的 final 方法实现的。这三个方法仅在 synchronized 方法中才能被调用。

2. wait()方法告知被调用的线程退出监视器并进入等待状态,直到其他线程进入相同的监视器并调用 notify( ) 方法。

3. notify( ) 方法通知同一对象上第一个调用 wait( )线程。

4. notifyAll() 方法通知调用 wait() 的所有线程,具有最高优先级的线程将先运行。

我们可以通过Java代码实现生产者消费者模型辅助理解wait-notify机制

public class WaitNotify {
    public static void main(String[] args) {
        Factory factory = new Factory();
        new Thread(new Producer(factory)).start();
        new Thread(new Consumer(factory)).start();
    }
}

class Producer implements Runnable{
    private Factory factory;
    public Producer(Factory factory){
        this.factory = factory;
    }
    @Override
    public void run() {
        for(int i = 1;i <= 50;i++){
            this.factory.add(i);
        }
    }
}
class Consumer implements Runnable{
    private Factory factory;
    public Consumer(Factory factory){
        this.factory = factory;
    }
    @Override
    public void run() {
        for(int i = 1;i <=50;i++){
            this.factory.del();
        }
    }
}
// 供货工厂
class Factory{
    // 保存货物
    int[] foods = new int[10];
    // 计数器 记录当前工厂商品数量
    int count = 0;
    // 生产
    public synchronized void add(int food){
        // 判断商品库存是否达到极限
        if(foods.length == count){
            try {
                // 生产者不再生产
                this.wait();
            } catch (InterruptedException e) {
                e.printStackTrace();
            }
        }
        if(count < foods.length){
            // 生产
            foods[count] = food;
            count++;
            System.out.println("生产者生产第" + food + "只炸鸡");
            // 叫消费来吃鸡
            this.notifyAll();
        }
    }
    // 消费
    public synchronized void del(){
        if(count == 0){
            try {
                // 通知消费者休息
                this.wait();
            } catch (InterruptedException e) {
                throw new RuntimeException(e);
            }
        }
        if(count != 0){
            int food = foods[--count];
            System.out.println("消费者吃了第" + food + "只炸鸡");
            this.notifyAll();
        }
    }
}

注意:wait和sleep的区别。wait()方法和sleep方法,都能让线程暂时停止运行。但是它们使用的方式和时机是不同的。

1. sleep方法在线程类Thread中定义,wait方法在Object中定义。

2. wait方法只能放在同步方法或同步块中,表示当前线程对资源进行等待。sleep方法可以放在任何位置,表示当前线程休眠。

3. wait方法要释放对象锁,sleep方法不会释放对象锁。

4. wait方法使用后,线程需要notify唤醒后才能继续执行。而sleep在休眠结束后,线程自动继续执行。

加锁的其他方式Lock接口

- Lock接口是java解决线程安全的另一种形式。

相对于synchronized,有以下特点:

1. Lock是一个接口,而synchronized是Java内置的语言实现;

2. synchronized在发生异常时,会自动释放线程占有的锁,因此不会导致死锁现象发生;而Lock在发生异常时,如果没有主动通过unlock()去释放锁,则很可能造成死锁现象,因此使用lock时需要在finally块中释放锁;

3. Lock可以让等待锁的线程响应中断,而synchronized却不行,使用synchronized时,等待的线程会一直等待下去,不能够响应中断;

4. 通过Lock可以知道有没有成功获取锁,而synchronized无法办到;

5. lock可以提高多个线程进行读操作的效率

public class LockTest {
    public static void main(String[] args) {
        LockObj obj = new LockObj();
        new Thread(new Runnable() {
            @Override
            public void run() {
                obj.speak();
            }
        },"第一线程").start();
        new Thread(new Runnable() {
            @Override
            public void run() {
                obj.speak();
            }
        },"第二线程").start();
    }
}

class LockObj {
    private Lock lock = new ReentrantLock();
    public void speak() {
        // 通过Lock接口的实现类对象 获取锁
        if(lock.tryLock()){
            try {
                for (int i = 0; i < 50; i++) {
//                Thread.currentThread().getName()  可获取当前线程名称
                    System.out.println(Thread.currentThread().getName() + "speak" + i);
                    Thread.sleep(30);
                }
            } catch (Exception e) {
                e.printStackTrace();
            } finally {
                // 释放锁
                lock.unlock();
            }
        }else {
            System.out.println("获取锁失败");
        }
    }
}

// lock()方法可以获取锁,但无返回值
// trylock()方法是有返回值的,它表示用来尝试获取锁,如果获取成功,则返回true
// unlock()方法可以释放锁

读写Lock

当有多个线程读写文件时,读操作和写操作会发生冲突现象,写操作和写操作会发生冲突现象,但是读操作和读操作不会发生冲突现象。

采用synchronized关键字来实现同步的话,就会导致一个问题:如果多个线程都只是进行读操作,所以当一个线程在进行读操作时,其他线程只能等待无法进行读操作。

因此就需要一种机制来使得多个线程都只是进行读操作时,线程之间不会发生冲突,通过读写锁可以办到。

读写锁将对一个资源(比如文件)的访问分成了2个锁,一个读锁和一个写锁。如果多个线程都只是进行读操作,所以当一个线程在进行读操作时,其他线程可以同时进行读操作。申请写操作的线程只能等待。如果有一个线程已经占用了写锁,则此时其他线程如果申请写锁或者读锁,则申请的线程会一直等待释放写锁。

public class RWLock {

    public static void main(String[] args) {
        RW rw = new RW();
        new Thread(new Runnable() {
            @Override
            public void run() {
                rw.write();
            }
        },"刘六").start();
        new Thread(new Runnable() {
            @Override
            public void run() {
                rw.read();
            }
        },"张三").start();

    }
}

class RW{
    // 创建读写Lock对象
    private ReentrantReadWriteLock rwLock = new ReentrantReadWriteLock();
    // 读
    public void read(){
        // 获取读锁
        rwLock.readLock().lock();
        try {
            for(int i = 0;i < 20;i++){
                System.out.println(Thread.currentThread().getName() + "读" + i);
                Thread.sleep(1);
            }
        }catch (Exception e){
            e.printStackTrace();
        }finally {
            rwLock.readLock().unlock();
        }
    }
    // 写
    public void write(){
        rwLock.writeLock().lock();
        try {
            for(int i = 0;i < 20;i++) {
                System.out.println(Thread.currentThread().getName() + "写" + i);
            }
        }catch (Exception e){
            e.printStackTrace();
        }finally {
            rwLock.writeLock().unlock();
        }
    }
}

Volatile

volatile变量是Java语言提供的一种稍弱的同步机制,用来确保将变量的更新操作通知到其他线程。

在访问volatile变量时不会执行加锁操作,因此也就不会使执行线程阻塞,因此volatile变量是一种比sychronized关键字更轻量级的同步机制。

当一个变量定义为 volatile 之后,将具备两种特性:

1. 保证此变量对所有的线程的可见性,当一个线程修改了这个变量的值,volatile 保证了新值能立即同步到主内存

2. 禁止指令重排序优化

 

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

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

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

抵扣说明:

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

余额充值