Fork me on Gitee

Java中的多线程

Java中的多线程

1. 进程与线程的区别

进程是所有线程的集合,每一个线程是进程中的一条执行路径,线程只是一条执行路径。

2. 创建线程的三种方式

2.1. 继承Thread类创建线程类

  • 优点:编写简单,如果需要访问当前线程,无需使用Thread.currentThread()方法,直接使用this,即可获得当前线程。
  • 缺点:因为线程类已经继承了Thread类,所以不能再继承其他的父类。

2.2. 实现Runnable接口

  • 优点:线程类只是实现了Runable接口,还可以继承其他的类。在这种方式下,可以多个线程共享同一个目标对象,所以非常适合多个相同线程来处理同一份资源的情况,从而可以将CPU代码和数据分开,形成清晰的模型,较好地体现了面向对象的思想。
  • 缺点:编程稍微复杂,如果需要访问当前线程,必须使用Thread.currentThread()方法。

2.3. 通过Callable创建线程

  • Callable规定的方法是call(),Runnable规定的方法是run().
  • Callable的任务执行后可返回值,而Runnable的任务是不能返回值得
  • call方法可以抛出异常,run方法不可以,因为run方法本身没有抛出异常,所以自定义的线程类在重写run的时候也无法抛出异常
  • 运行Callable任务可以拿到一个Future对象,表示异步计算的结果。它提供了检查计算是否完成的方法,以等待计算的完成,并检索计算的结果。通过Future对象可以了解任务执行情况,可取消任务的执行,还可获取执行结果。

2.4. 线程的创建方式源码

3. 创建线程池的四种方式

3.1. newCachedThreadPool

  • 创建一个可缓存的线程池,如果线程池长度超过处理需求,可灵活回收空闲线程,若无可回收,则新建线程(参考1)

  • 缺点:大家一般不用是因为newCachedThreadPool 可以无线的新建线程,容易造成堆外内存溢出,因为它的最大值是在初始化的时候设置为 Integer.MAX_VALUE,一般来说机器都没那么大内存给它不断使用。当然知道可能出问题的点,就可以去重写一个方法限制一下这个最大值。

3.2. newFixedThreadPool

​ 创建一个定长线程池,可控制线程最大并发数,超出的线程会在队列中等待。

​ 定长线程池的大小最好根据系统资源进行设置。如Runtime.getRuntime().availableProcessors()。可参考PreloadDataCache。其实newFixedThreadPool()在严格上说并不会复用线程,每运行一个Runnable都会通过ThreadFactory创建一个线程

3.3. newScheduledThreadPool

​ 创建一个定长线程池,支持定时及周期性任务执行。

​ Executors.newScheduledThreadPool(5);与Timer 对比:Timer 的优点在于简单易用,但由于所有任务都是由同一个线程来调度,因此所有任务都是串行执行的,同一时间只能有一个任务在执行,前一个任务的延迟或异常都将会影响到之后的任务(比如:一个任务出错,以后的任务都无法继续)。
ScheduledThreadPoolExecutor的设计思想是,每一个被调度的任务都会由线程池中一个线程去执行,因此任务是并发执行的,相互之间不会受到干扰。需要注意的是,只有当任务的执行时间到来时,ScheduedExecutor 才会真正启动一个线程,其余时间 ScheduledExecutor 都是在轮询任务的状态。
通过对比可以发现ScheduledExecutorService比Timer更安全,功能更强大,在以后的开发中尽可能使用ScheduledExecutorService(JDK1.5以后)替代Timer

3.4. newSingleThreadExecutor

​ 创建一个单线程化的线程池,它只会唯一的工作线程来执行任务,保证所有任务按照指定

​ 顺序(FIFO,LIFO,优先级)执行。

​ 现行大多数GUI程序都是单线程的。Android中单线程可用于数据库操作,文件操作,应用批量安装,应用批量删除等不适合并发但可能IO阻塞性及影响UI线程响应的操作。

3.5. 为什么要使用线程池

  • new Thread 的弊端

    • 每次new Thread新建对象性能差。
    • 线程缺乏统一管理,可能无限制新建线程,相互之间竞争,及可能占用过多系统资源导致死机或oom。
    • 缺乏更多功能,如定时执行、定期执行、线程中断。
  • 相比new Thread,Java提供的四种线程池的好处在于:

    • 重用存在的线程,减少对象创建、消亡的开销,性能佳。
    • 可有效控制最大并发线程数,提高系统资源的使用率,同时避免过多资源竞争,避免堵塞。
    • 提供定时执行、定期执行、单线程、并发数控制等功能。

3.6. 线程数的规划

  • CPU密集型任务(批量审核属于该类型任务)
    一般配置线程数=CPU总核心数+1 (+1是为了利用等待空闲)。
    要进行大量的计算,消耗CPU资源,比如计算圆周率、对视频进行高清解码等等,全靠CPU的运算能力。要最高效地利用CPU,计算密集型任务同时进行的数量应当等于CPU的核心数。
  • IO密集型任务
    一般配置线程数=CPU总核心数 * 2 +1。
    这类任务的CPU消耗很少,任务的大部分时间都在等待IO操作完成(因为IO的速度远远低于CPU和内存的速度)。常见的大部分任务都是IO密集型任务,比如Web应用。对于IO密集型任务,任务越多,CPU效率越高(但也有限度)。

4. 线程安全及解决

当多个线程同时共享,同一个全局变量或静态变量,做写的操作时,可能会发生数据冲突问题,也就是线程安全问题。做读操作是不会发生数据冲突问题。

使用线程同步或使用锁能解决线程安全问题,只能让当前一个线程进行执行。被包裹的代码执行完成后释放锁,让后才能让其他线程进行执行。这样的话就可以解决线程不安全问题。

  • 解决多线程同步问题:

    (1) 同步代码块(对象锁)

    (2) 同步函数(对象锁) ,修饰在方法上,多个线程调用同一个对象的同步方法会阻塞,调用不同对象的同步方法不会阻塞

    (3) 静态同步函数(类锁)

    (4) 使用lock

  • 同步代码块与同步函数区别?

    同步代码块使用自定锁(明锁)

    同步函数使用this锁

    如果多个线程使用同一个锁的话,那么两者均可以使用,如果存在多个锁的话,只能使用同步代码块

    同步代码块可以选择以什么来加锁,比同步方法更精确,我们可以选择只有会在同步发生同步问题的代码加锁,而并不是整个方法。

    public class SynObj{
        public synchronized void showA(){
            //只能使用当前对象进行加锁
            System.out.println("showA..");
            try {
                Thread.sleep(3000);
            } catch (InterruptedException e) {
                e.printStackTrace();
            }
        }
    
        public void showB(){
            synchronized (this) {
                System.out.println("showB..");
            }
        }
    
        public void showC(){
            //使用自定义的字符串来加锁
            String s="1";
            synchronized (s) {
                System.out.println("showC..");
            }
        }
    }
    
    
    public class Test {
        public static void main(String[] args) {
            final SynObj sy=new SynObj();
            new Thread(new Runnable() {  
                @Override
                public void run() {
                    sy.showA();
                }
            }).start();
            new Thread(new Runnable() {
                @Override
                public void run() {
                    sy.showB();
                }
            }).start();
            new Thread(new Runnable() {  
                @Override
                public void run() {
                    sy.showC();
                }
            }).start();
        }
    }
    
    代码的打印结果是,showA…..showC…..会很快打印出来,showB…..会隔一段时间才打印出来
    
  • 同步函数与静态同步函数区别?

    注意:

    面试会这样问:例如现在一个静态方法和一个非静态方法怎么实现同步?

    同步函数使用this锁(实例对象本身)

    静态同步函数使用字节码文件,也就是类.class(类对象本身)

  • 使用lock

    synchronized是在JVM层面实现的,因此系统可以监控锁的释放与否,而ReentrantLock使用代码实现的,系统无法自动释放锁,需要在代码中finally子句中显式释放锁lock.unlock();

    在并发量比较小的情况下,使用synchronized是个不错的选择,但是在并发量比较高的情况下,其性能下降很严重,此时ReentrantLock是个不错的方案。

  • lock与synchronized的区别

    • 首先synchronized是java内置关键字,在jvm层面,Lock是个java类;
    • synchronized无法判断是否获取锁的状态,Lock可以判断是否获取到锁;
    • synchronized会自动释放锁(a 线程执行完同步代码会释放锁 ;b 线程执行过程中发生异常会释放锁),Lock需在finally中手工释放锁(unlock()方法释放锁),否则容易造成线程死锁;而Lock锁就不一定会等待下去,如果尝试获取不到锁,线程可以不用一直等待就结束了;
    • synchronized的锁可重入、不可中断、非公平,而Lock锁可重入、可判断、可公平(两者皆可)
    • Lock锁适合大量同步的代码的同步问题,synchronized锁适合代码少量的同步问题。
    • 最重要的是Lock是一个接口,而synchronized是一个关键字,synchronized放弃锁只有两种情况:①线程执行完了同步代码块的内容②发生异常;而lock不同,它可以设定超时时间,也就是说他可以在获取锁时便设定超时时间,如果在你设定的时间内它还没有获取到锁,那么它会放弃获取锁然后响应放弃操作。

    Lock锁,可以得到和 synchronized一样的效果,即实现原子性、有序性和可见性。
    相较于synchronized,Lock锁可手动获取锁和释放锁、可中断的获取锁、超时获取锁。
    Lock 是一个接口,两个直接实现类:ReentrantLock(重入锁), ReentrantReadWriteLock(读写锁)。

4.1. synchronized加锁原理

Java虚拟机中,synchronized支持的同步方法和同步语句都是使用monitor来实现的。每个对象都与一个monitor相关联,当一个线程执行到一个monitor监视下的代码块中的第一个指令时,该线程必须在引用的对象上获得一个锁,这个锁是monitor实现的。在HotSpot虚拟机中,monitor是由ObjectMonitor实现,使用C++编写实现,具体代码在HotSpot虚拟机源码ObjectMonitor.hpp文件中。

查看源码会发现,主要的属性有_count(记录该线程获取锁的次数)、_recursions(锁的重入次数)、_owner(指向持有ObjectMonitor对象的线程)、_WaitSet(处于wait状态的线程集合)、_EntryList(处于等待锁block状态的线程队列)。

当并发线程执行synchronized修饰的方法或语句块时,先进入_EntryList中,当某个线程获取到对象的monitor后,把monitor对象中的_owner变量设置为当前线程,同时monitor对象中的计数器_count加1,当前线程获取同步锁成功。

当synchronized修饰的方法或语句块中的线程调用wait()方法时,当前线程将释放持有的monitor对象,monitor对象中的_owner变量赋值为null,同时,monitor对象中的_count值减1,然后当前线程进入_WaitSet集合中等待被唤醒。

public class Lock{
    boolean isLocked = false;
    Thread  lockedBy = null;
    int lockedCount = 0;
    public synchronized void lock() throws InterruptedException{
        Thread callingThread = Thread.currentThread();
        while(isLocked && lockedBy != callingThread){
            wait();
        }
        isLocked = true;
        lockedCount++;
        lockedBy = callingThread;
    }
    public synchronized void unlock(){
        if(Thread.curentThread() == this.lockedBy){
            lockedCount--;
            if(lockedCount == 0){
                isLocked = false;
                notify();
            }
        }
    }
}

4.2. 读写锁

读写锁实际是一种特殊的自旋锁,它把对共享资源的访问者划分成读者和写者,读者只对共享资源进行读访问,写者则需要对共享资源进行写操作。

何谓自旋锁?它是为实现保护共享资源而提出一种锁机制。其实,自旋锁与互斥锁比较类似,它们都是为了解决对某项资源的互斥使用。

无论是互斥锁,还是自旋锁,在任何时刻,最多只能有一个保持者,也就说,在任何时刻最多只能有一个执行单元获得锁。但是两者在调度机制上略有不同。

对于互斥锁,如果资源已经被占用,资源申请者只能进入睡眠状态。

但是自旋锁不会引起调用者睡眠,如果自旋锁已经被别的执行单元保持,调用者就一直循环在那里看是否该自旋锁的保持者已经释放了锁,"自旋"一词就是因此而得名。

自旋就是在循环判断条件是否满足,那么会有什么问题吗?如果锁被占用很长时间的话,自旋的线程等待的时间也会变长,白白浪费掉处理器资源。因此在JDK中,自旋操作默认10次,我们可以通过参数“-XX:PreBlockSpin”来设置,当超过来此参数的值,则会使用传统的线程挂起方式来等待锁释放。

4.3. 重入锁与不可重入锁(自旋锁)

在Java中,ReentrantLock和Synchronized都是可重入锁。

重入锁(可一定程度避免死锁)
就如同在饭堂打饭,你在窗口排着队。排到你的时候,突然路人A让你顺带着打个饭吧,然后你就打了两份饭,这时候你还没离开窗口,又有路人B让你打一份汤,于是你又额外打了一份汤。

即:可重入锁,也叫做递归锁,指的是同一线程 外层函数获得锁之后 ,内层递归函数仍然有获取该锁的代码,但不受影响。(一人得道,鸡犬升天。)

不可重入锁(自旋锁)
在另一个菜式比较好吃且热门的窗口,可不是这样的,在这里你在窗口,只能点一个菜(进入一次临界区),点完后,你想要再点别的菜,只能重新排一次队(虽然可以插队,当然我们可以引入服务员队伍管理机制:private Lock windows = new ReentrantLock(true);,指定该锁是公平的。)

即:自旋锁是专为防止多处理器并发而引入的一种锁,它在内核中大量应用于中断处理等部分。

5. 多线程锁

多线程锁包括死锁、活锁、饥饿、无锁。

死锁、活锁、饥饿是关于多线程是否活跃出现的运行阻塞障碍问题,如果线程出现 了这三种情况,即线程不再活跃,不能再正常地执行下去了。
死锁
死锁是多线程中最差的一种情况,多个线程相互占用对方的资源的锁,而又相互等 对方释放锁,此时若无外力干预,这些线程则一直处理阻塞的假死状态,形成死锁。 举个例子,A 同学抢了 B 同学的钢笔,B 同学抢了 A 同学的书,两个人都相互占 用对方的东西,都在让对方先还给自己自己再还,这样一直争执下去等待对方还而 又得不到解决,老师知道此事后就让他们相互还给对方,这样在外力的干预下他们 才解决,当然这只是个例子没有老师他们也能很好解决,计算机不像人如果发现这 种情况没有外力干预还是会一直阻塞下去的。

public class Demo {
    public static Demo a = new Demo();
    public static Demo b = new Demo();
    public void fun1(){
        synchronized(a) {
            System.out.println("fun1");
            try {
                Thread.sleep(100);
            } catch (InterruptedException e) {}
            b.fun2();
        }
    }
    public void fun2(){
        synchronized(b) {
            System.out.println("fun2");
            try {
                Thread.sleep(100);
            } catch (InterruptedException e) {}
            a.fun1();
        }
    }
}

fun1方法中,锁住了a对象,使用b对象。
fun2方法中,锁住了b对象,使用a对象。
这就是互相等待。

活锁
活锁这个概念大家应该很少有人听说或理解它的概念,而在多线程中这确实存在。 活锁恰恰与死锁相反,死锁是大家都拿不到资源都占用着对方的资源,而活锁是拿 到资源却又相互释放不执行。当多线程中出现了相互谦让,都主动将资源释放给别 的线程使用,这样这个资源在多个线程之间跳动而又得不到执行,这就是活锁。
饥饿
我们知道多线程执行中有线程优先级这个东西,优先级高的线程能够插队并优先执 行,这样如果优先级高的线程一直抢占优先级低线程的资源,导致低优先级线程无 法得到执行,这就是饥饿。当然还有一种饥饿的情况,一个线程一直占着一个资源 不放而导致其他线程得不到执行,与死锁不同的是饥饿在以后一段时间内还是能够 得到执行的,如那个占用资源的线程结束了并释放了资源。
无锁
无锁,即没有对资源进行锁定,即所有的线程都能访问并修改同一个资源,但同时 只有一个线程能修改成功。无锁典型的特点就是一个修改操作在一个循环内进行, 线程会不断的尝试修改共享资源,如果没有冲突就修改成功并退出否则就会继续下 一次循环尝试。所以,如果有多个线程修改同一个值必定会有一个线程能修改成功, 而其他修改失败的线程会不断重试直到修改成功。 可以看出,无锁是一种非常良好的设计,它不会出现线程出现的跳跃性问题,锁使 用不当肯定会出现系统性能问题,虽然无锁无法全面代替有锁,但无锁在某些场合 下是非常高效的。

6. Wait()与Notify ()区别

Wait让当前线程有运行状态变为等待状态,和同步一起使用

Notify 唤醒现在正在等待的状态,和同步一起使用

7. Wait()与sleep()区别

sleep()方法,该方法是属于Thread类中的。wait()方法,则是属于Object类中的。

sleep是Thread的静态类方法,谁调用的谁去睡觉,即使在a线程里调用了b的sleep方法,实际上还是a去睡觉,要让b线程睡觉要在b的代码中调用sleep。

在调用sleep()方法的过程中,线程不会释放对象锁。sleep()方法导致了程序暂停执行指定的时间,但是他的监控状态依然保持者,当指定的时间到了又会自动恢复运行状态。 而wait方法释放了锁,使得其他线程可以使用同步控制块或者方法。

sleep不出让系统资源;wait是进入线程等待池等待,出让系统资源,其他线程可以占用CPU。一般wait不会加时间限制,因为如果wait线程的运行资源不够,再出来也没用,要等待其他线程调用notify/notifyAll唤醒等待池中的所有线程,才会进入就绪队列等待OS分配系统资源。sleep(milliseconds)可以用时间指定使它自动唤醒过来,如果时间不到只能调用interrupt()强行打断。

使用范围:wait,notify和notifyAll只能在同步控制方法或者同步控制块里面使用,而sleep可以在任何地方使用

synchronized(x){
    x.notify()
    //或者wait()
}

sleep必须捕获异常,而wait,notify和notifyAll不需要捕获异常 。

理解:

sleep由线程自动唤醒,wait必须显示用代码唤醒。

如果线程调用了对象的 wait()方法,那么线程便会处于该对象的等待池中,等待池中的线程不会去竞争该对象的锁。
当有线程调用了对象的 notifyAll()方法(唤醒所有 wait 线程)或 notify()方法(只随机唤醒一个 wait 线程),被唤醒的的线程便会进入该对象的锁池中,锁池中的线程会去竞争该对象锁。
优先级高的线程竞争到对象锁的概率大,假若某线程没有竞争到该对象锁,它还会留在锁池中,唯有线程再次调用 wait()方法,它才会重新回到等待池中。而竞争到对象锁的线程则继续往下执行,直到执行完了 synchronized 代码块,它会释放掉该对象锁,这时锁池中的线程会继续竞争该对象锁。

8. start()和run()的区别

  • start()方法用来,开启线程,但是线程开启后并没有立即执行,他需要获取cpu的执行权才可以执行
  • run()方法是由jvm创建完本地操作系统级线程后回调的方法,不可以手动调用(否则就是普通方法)
  • start()方法让一个线程进入就绪队列等待分配cpu,分到cpu后才调用实现的run()方法。

9. join 方法

可以将并行改为串行,在A线程中调用了B线程的join()方法时,表示只有当B线程执行完毕时,A线程才能继续执行

public class JoinTest {
    public static void main(String [] args) throws InterruptedException {
        ThreadJoinTest t1 = new ThreadJoinTest("线程A");
        ThreadJoinTest t2 = new ThreadJoinTest("线程B");
        t1.start();
        /**join的意思是使得放弃当前线程的执行,并返回对应的线程,例如下面代码的意思就是:
         程序在main线程中调用t1线程的join方法,则main线程放弃cpu控制权,
         并返回t1线程继续执行直到线程t1执行完毕
         所以结果是t1线程执行完后,才到主线程执行,
         相当于在main线程中同步t1线程,t1执行完了,main线程才有执行的机会
         */
        t1.join();
        t2.start();
    }

}
class ThreadJoinTest extends Thread{
    public ThreadJoinTest(String name){
        super(name);
    }
    @Override
    public void run(){
        for(int i=0;i<1000;i++){
            System.out.println(this.getName() + ":" + i);
        }
    }
}

》》程序结果是先打印完线程A线程,在打印线程B线程;  

10. sleep()与yield()的区别

sleep:会强制让当前线程进入等待,即当前线程的状态为:等待、阻塞

yield:会先去判断是否有和当前线程相同优先级的线程,如果没有,则自己继续执行,如果有,则将CPU资源让给它,然后进入到就绪状态。

11. 说一下ThreadLocal

  1. ThreadLocal是Java提供的本地存储机制,利用这个机制可以将数据缓存在线程内部,在任何时候都可以获取使用。
  2. ThreadLocal底层是通过ThreadLocalMap来实现的,每一个Thread对象中都存在一个ThreadLocalMap,Map的Key是ThreadLocal对象,Map的Value是缓存的值。
  3. 如果在线程池中使用ThreadLocal会造成内存泄露的问题,需要手动进行remove。因为线程池中的线程不会回收,而是去执行下一个任务,线程不被回收,Entry对象就不会被回收,造成了内存泄露。

12. Java中的原子类型

原子操作是指不会被线程调度机制打断的操作,这种操作一旦开始,就一直运行到结束,中间不会有任何线程上下文切换。
Java从JDK 1.5开始提供了java.util.concurrent.atomic包,这个包中的原子操作类提供了一种用法简单、性能高效、线程安全地更新一个变量的方式。
Atomic包大致可以属于4种类型的原子更新方式,分别是原子更新基本类型、原子更新数组、原子更新引用、原子更新属性。比如AtomicInteger、AtomicLong、AtomicBoolean等。

volatlie关键字有可见性、有序性,没有原子性,那么value+=1这行代码实则会被分为4步执行

获取value的值

将获取到的值+1

将最新值赋值给value

将value的值刷入内存

假设当时value值为1,当线程1执行完+=操作的第1步时,cpu执行权被线程2抢走,然后线程2执行+=操作,直至输入内存,并输出2,这时cpu执行被线程1抢走,继续执行没有完成的+=操作,那么这时线程1会根据第一步拿到的1进行+1操作,那么返回输出的同样是2。

volatlie关键字保证了不同线程对这个变量进行操作时的可见性,即一个线程修改了某个变量的值(只有在修改的情况下,只读取了不生效),这新值对其他线程来说是立即可见的。

volatlie关键字禁止进行指令重排序。

参考资料

posted @ 2021-03-14 22:28  明叶师兄。  阅读(137)  评论(0)    收藏  举报