多线程、线程同步知识汇总

多线程

程序 进程 线程

  • 程序:静态的代码,没有运行起来的才叫程序
    eg:文件夹中的所有文件
    整个文件夹内包括的所有静态代码的集合就是程序

  • 进程:是程序的一次运行or正在运行的一个程序,是一个动态的过程,并且有自身的产生、存在、消亡的过程——生命周期
    eg:运行中的QQ、运行中的LOL、运行中的360安全卫士

  • 线程:进程可以细分为线程,是程序内部的一条执行路径。如果一个进程可以同时并行执行多个线程,就是支持多线程的。
    线程是调度和执行的单位,有独立的运行栈和程序计数器,线程切换的开销小
    一个进程中多个线程共享相同的资源,因此可以进程间通信更高效,但也带来了安全隐患
    eg:同时扫漏洞、扫毒、清理垃圾就是多线程,多线程增加了cpu利用率
    eg:图形化界面的基本都是多线程
    eg:QQ同时有很多人互相发消息

  • 一个java应用程序java.exe至少有三个线程:main()主线程、gc()垃圾回收线程、异常处理线程,还可以有内存监控 操作日志

CPU的单核和多核

  • 单核CPU:实际上是一种假的多线程,表象的多线程实际上是快速在不同线程之间切换(时分复用),是一种并发

eg:同一个进程,使用单核cpu,单线程比多线程更快,因为没有线程切换时间

  • 多核CPU:可以实现并行,且

eg:现在的很多手机都是8核心,但这8核并不是完全相同,当手机写备忘录时,调用功耗小功能更弱的核,玩游戏时调用更强但功耗大的核

并发、并行

并发:一个CPU同时执行多个线程

并行:多个CPU同时执行多个线程

多线程的创建和使用

传统多线程

  • 线程具有优先级1~10,且是一个统计学上的优先级,满足大数定律
  • Thread类实现了Runnable接口,可以创建线程
  • 开发中常用的是线程池,线程池减少了线程的创建和销毁的用时
  • 在自己手写多线程时,可能出现逻辑正确,但有几个输出特例不符合多线程,这可能是由于多线程运行到屏幕输出的不稳定时间差造成的

Thread类的常用方法

  • void start(): 启动线程,并执行对象的run()方法
  • void run(): 线程在被调度时执行的操作
  • String getName(): 返回线程的名称
  • void setName(String name):设置该线程名称
  • static Thread currentThread(): 返回当前线程对象。在Thread子类中就 是this,通常用于主线程和Runnable Callable实现类
  • static void yield():线程让步。暂停当前正在执行的线程,把执行机会让给优先级相同或更高的线程 若队列中没有同优先级的线程,忽略此方法。
  • join() :当某个程序执行流中调用其他线程的 join() 方法时,调用线程将 被阻塞,直到 join() 方法加入的 join 线程执行完为止,低优先级的线程也可以获得执行。该方法需要try-catch
  • static void sleep(long millis):(指定时间:毫秒) 令当前活动线程在指定时间段内放弃对CPU控制,使其他线程有机会被执行,时间到后重排队。 抛出* InterruptedException异常。该方法需要try-catch
  • stop(): 强制线程生命期结束,不推荐使用
  • boolean isAlive():返回boolean,判断线程是否还活着

sleep yield join wait notify的区别放在后面的线程同步中

Runnable接口:作为Thread对象的构造形参

  1. 相比直接使用Thread,可以实现“多继承”
  2. 线程的开启仍需要实现Thread类
  3. new Thread( new MyThread() ) .start来启动

示例:

class MyrunThread implements Runnable {
 
    public void run() {
        for (int i = 1; i < 100; i++) {
            System.out.println(Thread.currentThread().getName() + ":" + i);
        }
    }
}
public class RunnableTest {
    public static void main(String[] args) {
 	//一步写法
        new Thread(new MyrunThread()).start();
        //两步写法
        MyrunThread m1 = new MyrunThread();
        Thread M1 = new Thread(m1);
        //M1可以调用Thread类的所有方法
        M1.start();
    }
}

Callable接口:借助FutureTask解耦合

  1. 与Runnable相比,Callable功能更强大,可以有返回值,可以抛异常
  2. Runnable是重写run()方法,Callable是重写call()方法
  3. 返回值的功能需要借助FutureTask类,并且从FutureTask类对象.get()获取返回值
  4. 因为即便是用Callable接口实现,也存在不需要返回值的情况,因此引入FutureTask类来解耦合
  5. new Thread( new FutureTask( new MyThread() ) ).start来启动

实现Callable接口,重写call()方法

class Mythread implements Callable<Integer>{
 
    @Override
    public Integer call() throws Exception {
int  sum = 0;
        for (int  i = 1 ; i<100 ; i++){
            if(i%4==0){
                System.out.println(i);
                sum+=i;
            }
        }
        return sum;
        //这里的返回值是不能通过Thread直接获取的
        //需要利用FutureTask
    }
}

借助FutureTask,实现Thread

public class CallableTest {
    public static void main(String[] args) throws ExecutionException, InterruptedException {
       Mythread M1 = new Mythread();//实现Callable接口实例
       FutureTask F1 = new FutureTask(M1);//传入FutureTask中
       Thread T1 = new Thread(F1);//传入Thread中
        T1.start();//启动
        System.out.println(F1.get());
 
        //start运行call方法,但是不一定必须接收返回值
    }
}

线程池

与传统相比的优点

  • 传统的线程实现方式有包含了线程创建 线程销毁 的时间,而线程是可以复用的,因此引入线程池,把线程创建和销毁 改为 用后归还,不销毁,重复利用资源,提高线程利用率,提高程序的响应速度
  • 便于统一管理线程对象
  • 可以控制最大并发量

线程池模型——银行

柜台窗口:线程,且不销毁
等待区:等待队列
窗口不够用时:增加线程
等待区没有座位时:拒绝策略——报异常或交给某个线程去左
窗口空闲太多:减少线程

线程池创建

JUC并发工具包提供了ThreadPoolExecutor创建线程池,ThreadPoolExecutor有8个参数,分别为:

  • corePoolSize:核心线程数——永不关闭的柜台窗口数
  • maximumPoolSize:最大线程数——最多的窗口总数
  • keepAliveTime:线程存活时间——非核心线程过多长时间没有接到任务就销毁——需要设置单位unit
  • unit:TimeUnit.SECONDS 或TimeUnit.MICROSECONDS等等都可以
  • BlockingQueue :等待队列,new ArrayBlockingQueue<>(capacity:10)这里capacity的值设为10
  • ThreadFactory:线程工厂——无需自己创建,直接调用Executors.defaultThreadFactory()
  • RejectedExecutionHandler:拒绝策略——new ThreadPoolExecutor.AbortPolicy()被拒绝的任务的处理程序,抛出一个 RejectedExecutionException

创建好之后用ExecutorService类对象来接收,之后就可以直接调用了
例如:利用lambda表达式

for(){
executorService.execute( ()->{ 线程业务逻辑 } );
}//线程池会自动调整当前线程数
executorService.shutdown();//关闭线程池

参考:
拒绝策略实现步骤

线程池详解

线程同步

线程同步的同步意为:协同步调,按预定的先后次序进行运行
防止因为并发导致的数据读写错误

线程的生命周期

锁机制

同步的实现离不开锁机制,以下几种常见的锁
参考资料

死锁

参考:死锁的预防
死锁的预防一般有:
hashCode指定顺序Lock接口超时放弃

手写死锁:

class Mythread implements Runnable{
public void run(){
......
synchronized(锁A){
//A锁套B锁
    synchronized(锁B){
     ......
    }
}
......
}
}
 
class Youthread implements Runnable{
public void run(){
......
synchronized(锁B){
//B锁套A锁
    synchronized(锁A){
     ......
    }
}
......
}
}

如果是锁进行了递归调用,一个锁也是可能产生死锁的

悲观锁

  • 认为总有线程会操作自己正在操作的数据
  • 使用场景:频繁写入;保证安全性
  • 锁实现:synchronized关键字、Lock接口

乐观锁

  • 认为没有线程会操作自己正在操作的数据
  • 使用场景:多读少写;保证效率高
  • 锁实现:CAS算法

读锁(S锁)(共享锁)

  • 只读,例如SELECT
  • 加S锁后,别的事务只能再加S锁而不能加X锁,但加锁者本身可以再加X锁

若事务T对数据对象A加上S锁,则事务T可以读A但不能修改A,其他事务只能再对A加S锁,而不能加X锁,直到T释放A上的S锁。这保证了其他事务可以读A,但在T释放A上的S锁之前不能对A做任何修改。

写锁(X锁)(排他锁)

  • 写,例如INSERT、UPDATE 或 DELETE
  • 加X锁后,其他任何事务不能再加任何S锁和X锁

若事务T对数据对象A加上X锁,事务T可以读A也可以修改A,其他事务不能再对A加任何锁,直到T释放A上的锁。这保证了其他事务在T释放A上的锁之前不能再读取和修改A。

公平锁

  • 多个共用同一把锁的线程有序排队,并按排队顺序上锁
  • private ReentrantLock lockkk = new ReentrantLock(true);叫公平锁

手写公平锁:

private  ReentrantLock lockkk = new ReentrantLock(true);//公平锁
。。。。。。。。。
    public void run(){
        for(int i = 1;i<=100;i++){
            lockkk.lock();//上公平锁lockkk
            System.out.print(Thread.currentThread().getName());
            Depot(this.account,1000);
            //输出内容
            try {
            Thread.sleep(100);
        } catch (InterruptedException e) {
            e.printStackTrace();
        }//加上sleep效果更明显,防止因为屏幕输出时间差造成的误导
 
            lockkk.unlock();//释放公平锁lockkk

//此后该调用lock的线程进入排队的末尾,不与其他线程争抢资源
        }
}

非公平锁

  • 多个共用同一把锁的线程有序排队,但不按顺序上锁,仍像synchronized一样靠抢
  • private ReentrantLock lockkk = new ReentrantLock(false);
    private ReentrantLock lockkk = new ReentrantLock();都是非公平锁

volatile关键字

参考资料

  • 只能修饰变量,如:private volatile static int phoneNumber = 0;
  • 对变量的写操作不依赖于当前值,若是++i , i+=5这种依赖于原有值的操作则可能仍然会有错误
  • 该变量是独立的:该变量没有包含在具有其他变量的不变式中
  • volatile不需要加锁,比synchronized更轻量级,不会阻塞线程
  • synchronized既能保证可见性,又能保证原子性,而volatile只能保证可见性,无法保证原子性

synchronized关键字

为什么用对象来充当监视器?:
 由于java的每个对象都有一个内置锁,当用此关键字修饰方法时,内置锁会保护整个方法。在调用该方法前,要获取内置锁,否则处于阻塞状态。

同步方法(粗粒度锁)

  • 比同步代码块更笨重,开销更大
  • 用this上锁,因此也有潜在的风险
  • 同步方法(粗粒度锁):
      A、修饰一般方法: public synchronized void method(){…},获取的是当前调用对象 this 上的锁;
      B、修饰静态方法: public static synchronized void method (){…},获取当前类的字节码对象上的锁。

同步代码块(细粒度锁)

  • 对象监视器:同一个对象同一把锁,处于同一个同步机制里
  • 监视器用this指的是当前类对象,
    例如:用Runnable接口实现的线程,run()方法写为synchronized,那么这个锁就是Runnable的对象,如果用这一个对象作为形参创建了多个线程,就是线程同步的
    例如:用extends直接继承Thread,run()方法写为synchronized,那么new几个线程就加了几把锁,失去了同步功能

Lock接口

Lock接口可以主动在任意位置lock和unlock,所以更加灵活

ReentrantLock接口

public class ReentrantLock implements Lock, java.io.Serializable

  • 提供公平锁机制,而synchronized是非公平的
  • boolean tryLock() 方法:如果拿不到锁就返回false,不会像synchronized一直等待
  • void lock() 上锁,且需要void unlock() 主动解锁
  • 当发生异常时,synchronized会自动释放锁,而Lock接口需要在try-catch-finally中手动释放锁,防止死锁
  • 在性能上来说,如果线程竞争资源不激烈时,两者的性能是差不多的,而当竞争资源非常激烈时(即有大量线程同时竞争),此时 Lock 的性能要远远优于 synchronized

例子

private  ReentrantLock locker = new ReentrantLock(true);//公平锁
。。。。。。。。。
    public void run(){
        for(int i = 1;i<=100;i++){//模拟多线程
            locker.lock();//上公平锁lockkk
          try{执行的可能出错的代码
          }catch(Exception e){
			 e.printStackTrace();
		}finally{
			locker.unlock()
		}
        }
    }

sleep yield join wait notify

首先确定一点,CPU资源调度和线程同步不是同一个概念,即便是没有监视器(没有锁),也存在CPU资源调度问题(也有sleep和yield方法);
而只有加锁之后才有wait和notify来操作线程的执行

CPU资源角度

  • sleep和yield都不释放锁,因此影响的是cpu资源调度
  • 都是静态方法,因此可以无关监视器,无关同步

sleep线程休眠

  • 不释放锁,同监视器的线程仍然阻塞
  • 稳定让渡cpu资源,不同监视器的线程可以执行
  • 静态方法,Thread.sleep(123123)随处可用

yield线程让步

  • 不释放锁,同监视器的线程仍然阻塞
  • 不稳定让渡cpu资源,重新争夺cpu资源,可能没有明显效果
  • 其他监视器的线程需:优先级>=调用yield的线程,才能争夺cpu资源

线程同步角度

  • wait notify notifyAll 都是需要写在同步代码块之中的,针对已经获取了Obj锁进行操作,都不是静态方法
  • 从语法角度来说就是Obj.wait(),Obj.notify必须在synchronized(Obj){…}语句块内。
  • 从功能上来说wait就是说线程在获取对象锁后,主动释放对象锁,同时本线程休眠。直到有其它线程调用对象的notify()唤醒该线程,才能继续获取对象锁

wati

  • 释放CPU的同时释放锁
  • 进入等待池,等待其他线程使用obj.notify()唤醒
  • 调用obj.wait()后直接释放资源
  • 因为是写在synchronized代码块中,所以调用obj.wait()之后还需等其他线程obj.notifyAll()之后才执行wait之后剩余的代码

notify notifyAll

  • 在相同监视器下(相同锁下),前者是随机唤醒一个线程,后者是唤醒所有线程
  • notify和notifyAll的最终效果都是从等待池唤醒一个,但notify可能会导致两个线程都挂起,造成阻塞(所有线程都wait进入了等待池,没有线程再执行唤醒)

对象内部锁

其实,每个对象都拥有两个池,分别为锁池(EntrySet)和(WaitSet)等待池。

  • 锁池:假如已经有线程A获取到了锁,这时候又有线程B需要获取这把锁(比如需要调用synchronized修饰的方法或者需要执行synchronized修饰的代码块),由于该锁已经被占用,所以线程B只能等待这把锁,这时候线程B将会进入这把锁的锁池。
  • 等待池:假设线程A获取到锁之后,由于一些条件的不满足(例如生产者消费者模式中生产者获取到锁,然后判断队列为满),此时需要调用对象锁的wait方法,那么线程A将放弃这把锁,并进入这把锁的等待池。

如果有其他线程调用了锁的notify方法,则会根据一定的算法从等待池中选取一个线程,将此线程放入锁池。
如果有其他线程调用了锁的notifyAll方法,则会将等待池中所有线程全部放入锁池,并争抢锁。

锁池与等待池的区别:等待池中的线程不能获取锁,而是需要被唤醒进入锁池,才有获取到锁的机会。

参考资料1

参考资料2

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

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

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

抵扣说明:

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

余额充值