线程知识大全(权)

线程

CPU:中央处理器

1.1 进程和线程

  • 进程是指在运行的程序,确切的来说,就是当一个程序进入内存运行,即变成一个进程,进程是处于运行过程中的程序,并且具有一定独立功能。
  • 线程是进程中的一个执行单元,负责当前进程中程序的执行,一个进程中至少有一个线程。一个进程中是可以有多个线程的,这个应用程序也可以称为多线程程序。

总结:一个程序运行后至少有一个进程,一个进程中可以有多个线程

1.2 分时调度和抢占式调度

  • 分时调度:所有线程轮流使用CPU的使用权,平均分配每个线程占用CPU时间
  • 抢占式调度:优先让优先级高的线程使用CPU,如果线程的优先级相同,那么会随机选择一个(线程的随机性),java使用的是抢占式调度。

注:优先级高的线程抢夺CPU的执行时间长,CPU执行几率越大

1.3并发与并行

  • 并发:指在同一时间段只能有一条指令执行,但多个进程指令被快速轮换执行(交替执行)
  • 并行:指两个或多个事件在同一时刻发生,有多条指令在多个处理器上同时执行(同时执行)

1.4进程的特征

  • 独立性:进程是系统中独立存在的实体,它可以拥有自己独立的资源,每一个进程都拥有自己私有的地址空间。在没有经过进程本身允许的情况下,一个用户进程不可以直接访问其他进程的地址空间。
  • 动态性:进程与程序的区别在于,程序只是一个静态的指令集合,而进程是一个正在系统中活动的指令集合。在进程中加入了时间的概念。进程具有自己的生命周期和各种不同的状态,这些概念在程序中都是不具备的。
  • 并发性:多个进程可以在单个处理器上并发执行,多个进程之间不会互相影响。

线程:

  • main()方法的方法体就是主线程的线程执行体。
  • 一个进程可以拥有多个线程,一个线程必须有一个父线程。线程可以拥有自己的堆栈、自己的程序计数器和自己的局部变量,但不拥有系统资源,它与父进程的其他线程共享该进程所拥有的全部资源。因为多个线程共享父进程里的全部资源,因此编程更加方便;但要小心,因为需要确保线程不会妨碍同一进程里的其他线程。
  • 线程可以完成一定的任务,可以与其他线程共享父进程中的共享变量及部分环境,相互之间协同来完成所要完成的任务。
  • 线程是独立运行的,它并不知道进程中是否还有其他线程存在。线程的执行是抢占式的,当前运行的线程在任何时候都可能被挂起,以便另外一个线程可以运行。
  • 一个线程可以创建和撤销另一个线程,同一个进程中的多个线程之间可以并发执行。
  • 总的来说,一个程序运行后至少有一个进程,一个进程里可以包含多个线程,但至少包含一个线程。

问题:

线程对象调用run方法和调用start方法的区别?

  • 调用run方法不开启线程,仅是对象调用方法。
  • 调用start方法开启线程,并让JVM调用run()方法在开启的线程中执行。

1.5 多线程编程优点

  • 进程之间不能共享内存,但线程之间共享内存非常容易。
  • 系统创建进程时需要为该进程重新分配系统资源,但创建线程则代价小得多,因此使用多线程来实现多任务并发比多进程的效率高。
  • java语言内置了多线程功能支持,而不是单纯地作为底层操作系统的调度方式,从而简化了多线程编程。

2 线程的创建

2.1 Thread和Runnable的区别

  • 实现runnable接口比继承Thread类所具有的优势:
  1. 适合多个相同的程序代码去共享同一个资源。
  2. 可以避免java中的单继承的局限性。
  3. 增加程序的健壮性,实现解耦操作,代码可以被多个线程共享,代码和线程独立。
  4. 线程池只能放入实现Runnable或Callable类线程,不能直接放入继承Thread的类。

扩展:

  • 在java中,每次程序运行至少启动两个线程,一个是main线程,一个是垃圾回收线程,因为每当使用java命令执行一个类的时候,实际上都会启动一个JVM。每一个JVM其实就是在操作系统中启动了一个进程。

2.2线程的生命周期

  • 新建(New)、就绪、运行、阻塞、死亡五种状态。

2.2.1新建和就绪状态

当程序使用了new关键字创建了一个线程之后,该线程就处于新建状态,此时它和其他的java对象一样,仅仅有java虚拟机为其分配内存,并初始化其成员变量的值。此时的线程对象没有表现出任何线程的动态特征,程序也不会执行线程的线程执行体。

当线程对象调用了start()方法之后,该线程处于就绪状态,Java虚拟机会为其创建方法调用栈和程序计数器,处于这个状态中的线程并没有开始运行,只是表示该线程可以运行了。至少该线程何时运行,取决于JVM里线程调度器的调度。

注意:启动线程使用start()方法,而不是run()方法!永远不要调用线程对象的run()方法!调用start()方法来启动线程,系统会把该run()方法当成线程执行体来处理;但如果直接调用线程对象的run()方法,则run()方法立即就会被执行,而且在run()方法返回之前其他线程无法并发执行---也就是说,如果直接调用线程对象的run()方法,系统把线程对象当成一个普通对象,而run()方法也是一个普通方法,而不是线程的执行体。

如果希望调用子线程的start()方法后子线程立即开始执行,程序可以使用Thread.sleep(1)来让当前运行的线程(主线程)睡眠1毫秒就够了,因为在这1毫秒内CPU不会空闲,它会去执行另一个处于就绪状态的线程,这样就可以让子线程立即开始执行。

2.2.2运行和阻塞状态

运行状态:如果处于就绪状态的线程获得了CPU,开始执行run()方法的线程执行体,则该线程处于运行状态。

如果计算机只有一个CPU,那么在任何时候只有一个线程处于运行状态。当然,在一个多处理器的机器上,将会有多个线程并行执行;当线程数大于处理器数时,依然会存在多个线程在同一个CPU上轮换的现象。

运行进入阻塞状态:

  • 线程调用sleep()方法主动放弃所占用的处理器资源。
  • 线程调用了一个阻塞式IO方法,在该方法返回之前,该线程被阻塞。
  • 线程试图获得一个同步监视器,但该同步监视器正在被其他线程所持有。
  • 线程在等待某个通知(notify)、
  • 程序调用了线程的suspend()方法将该线程挂起,但这个方法容易导致死锁。

阻塞状态进入就绪状态:

  • 调用sleep()方法的线程经过了指定时间。
  • 线程调用的阻塞式IO方法已经返回。
  • 线程成功地获得了试图取得的同步监视器。
  • 线程正在等待某个通知时,其他线程发出了一个通知。
  • 处于挂起状态的线程被调用了resume()恢复方法。

注意:线程从阻塞状态只能进入就绪状态,无法直接进入运行状态。而就绪和运行状态之间的转换通常不受程序控制,而是由系统线程调度所决定,当处于就绪状态的线程获得处理器资源时,该线程进入运行状态;当处于运行状态的线程失去处理器资源时,该线程就进入就绪状态。但有一个方法例外,调用yield()方法可以让运行状态的线程转入就绪状态。

2.2.3线程死亡:

进入死亡状态:

  • run()或call()方法执行完成,结束后就处于死亡状态。
  • 线程抛出一个未捕获的Exception或Error。
  • 直接调用该线程的stop()方法来结束该线程--该方法容易导致死锁。

注意:

  • 当主线程结束时,其他线程不受任何影响,并不会随之结束。一旦子线程启动起来后,它就拥有和主线程相同的地位,它不会受主线程的影响。
  • 不要试图对一个已经死亡的线程调用start()方法使它重新启动,死亡就是死亡,该线程将不可再次作为线程执行。
  • 不要对死亡状态的线程调用start()方法,程序只能对新建状态的线程调用start()方法,对新建状态的线程两次调用start()方法也是错误的。这会引发IllegalThreadState exception(死亡状态的线程无法再次执行)异常。

2.3 控制线程

2.3.1 join线程

  • 概念:当在某个程序执行流中调用其他线程的join()方法时,调用线程将被阻塞,直到被join()方法加入的join线程执行完为止。

join()方法的重载

  • join():等待被join的线程执行完成。
  • join(long millis):等待非join方法的线程的时间最长为millis毫秒。如果在millis毫秒内非join方法的线程还没有执行结束,则不再等待。

2.3.2 后台线程

  • 功能:后台线程的任务是为其他的线程提供服务。例子:JVM的垃圾回收线程就是典型的后台线程。
  • 特征:如果所有的前台线程都死亡,后台线程会自动死亡。
  • 注意:前台线程死亡后,JVM会通知后台线程死亡,但从它接收指令到做出响应,需要一定时间。而且要将某个线程设置为后台线程,必须在该线程启动之前设置,也就是说setDaemon(true)必须在start()方法之前调用,否则会引发IllegalThreadState exception异常。

2.3.3 线程睡眠

  • 实现:如果需要让当前正在执行的线程暂停一段时间,并进入阻塞状态,可以通过调用Thread类的静态sleep()方法来实现。

重载形式:

  • static void sleep(long millis):让当前正在执行的线程暂停millis毫秒,并进入阻塞状态,该方法受到系统计时器和线程调度器的精度与准确度的影响。
  • static void sleep(long millis,int nanos):让当前正在执行的线程暂停millis毫秒加nanos毫微秒,并进入阻塞状态,该方法受到系统计时器和线程调度器的精度与准确度的影响。
  • 作用:当当前线程调用sleep()方法进入阻塞状态后,在其睡眠时间段内,该线程不会获得执行的机会,即使系统中没有其他可执行的线程,处于sleep()中的线程也不会执行,因此sleep()方法常用来暂停程序的执行。

2.3.4 改变优先级

  • 每个线程执行时都具有一定的优先级,优先级越高的线程获得较多的执行机会,而优先级低的线程则获得较少的执行机会。
  • 每个线程默认的优先级都与创建它的父线程的优先级相同,在默认情况下,main线程具有普通优先级,由main线程创建的子线程也具有普通优先级。

静态常量:

  • MAX_PRIORITY:其值是10。
  • MIN_PRIORITY:其值是1。
  • NORM_PRIORITY:其值是5。

设置优先级:

使用setPrority()方法来改变多线程的优先级。

方法名.setPrority(Thread.MAX_PRIORITY);

2.3.5 守护线程

thread.setDaemon(true);

还有一个启动守护线程的方法就是利用Timer和TimerTask ,Timer是JDK提供的定时器工具,使用时会在主线程之外单独起一个线程执行指定的任务。Timer timer = new Timer()启动的是用户线程,而Timer timer = new Timer(true)启动的就是守护线程。 TimerTask是一个实现了Runnable接口的抽象类,配合Timer使用可以看做被Timer执行的任务,即启动的线程。

特点: 守护线程拥有自动结束自己生命周期的特性,而非守护线程不具备这个特点。

2.4 线程安全问题

安全问题的产生:

  • 当我们使用多个线程访问同一资源的时候,一旦多个线程中对资源有写的操作,就容易出现线程安全问题。
  • 如果这个获取锁的线程由于要等待IO或者其他原因(比如调用sleep方法)被阻塞了,但是又没有释放锁,其 他线程便只能干巴巴地等待,试想一下,这多么影响程序执行效率。因此就需要有一种机制可以不让等待的线程一直无期限地等待下去(比如只等待一定的时间或者能够响应中断),通过Lock就可以办到
  • 当有多个线程读写文件时,读操作和写操作会发生冲突现象,写操作和写操作会发生冲突现象,但是读操作和读操作不会发生冲突现象。采用synchronized关键字来实现同步的话,就会导致一个问题:如果多个线程都只是进行读操作,所以当一个线程在进行读操作时,其他线程只能等待无法进行读操作。

2.5 同步方法

  • 与同步代码块对应,java的多线程安全支持提供了同步方法,同步方法就是使用synchronized关键字来修饰某个方法,则该方法称为同步方法。对于synchronized修饰的实例方法(非static方法)而言,无须显示指定同步监视器,同步方法的同步监视器是this,也就是调用该方法的对象。

2.6 线程安全的类具有的特征

  • 该类的对象可以被多个线程安全地访问。
  • 每个线程调用该对象的任意方法之后都将得到正确结果。
  • 每个线程调用该对象的任意方法之后,该对象状态依然保持合理状态。

2.7 减少线程安全所带的负面影响

  • 不要读线程安全类的所有方法都进行同步,只对那些会改变竞争资源(竞争资源也就是共享资源)的方法进行同步。
  • 如果可变类有两种运行环境:单线程环境和多线程环境,则应该为该可变类提供两种版本,即线程不安全版本和线程安全版本。在单线程环境中使用线程不安全版本以保证性能,在多线程环境中使用线程安全版本

2.8 释放对同步锁的锁定

  • 当前线程的同步方法、同步代码块执行结束,当前线程即释放同步监视器。
  • 当前线程在同步代码块、同步方法中遇到break、return终止了该代码块、该方法的继续执行,当前线程将会释放同步监视器。
  • 当前线程在同步代码块、同步方法中出现了未处理的Error或Exception,导致了该代码块、该方法异常结束时,当前线程将会释放同步监视器。
  • 当前线程执行同步代码块或同步方法时,程序执行了同步监视器对象的wait()方法,则当前线程暂停,并释放同步监视器。

2.9 不释放对同步锁的锁定

  • 线程执行同步代码块或同步方法时,程序调用Threa.sleep()、Thread.yield()方法来暂停当前对线程的执行,当前线程不会释放同步监视器。
  • 线程执行同步代码块时,其他线程调用了该线程的suspend()方法将该线程挂起,该线程不会释放同步监视器。程序应当尽量避免使用suspend()和resume()方法来控制线程。

2.10 死锁及处理策略

解决死锁问题:

  • 避免多次锁定:尽量避免同一个线程对多个同步监视器进行锁定。
  • 具有相同的加锁顺序:如果多个线程需要对多个同步监视器进行锁定,则应该保证它们以相同的顺序请求加锁。
  • 使用定时锁:程序调用Lock对象的tryLock()方法加锁时可指定time和unit参数,当超过指定时间后会自动释放对Lock的锁定。这样就可以解开死锁了、
  • 死锁检测:这是一种依靠算法来实现的死锁预防机制,它主要针对那些不可能实现按序加锁,也不能使用定时器锁的场景。

2.11 锁的类型

  • 可重入锁(synchronized和ReentrantLock)
  • 可中断锁(Lock)
  • 公平锁(ReentrantLock和ReentrantReadWriteLock),默认是非公平锁,可以设置为公平锁
  • 读写锁ReadWriteLock就是读写锁,它是一个接口,ReentrantReadWriteLock实现了这个接口。可以通过readLock()获取读锁,通过writeLock()获取写锁

2.12 线程通信

  • wait()、notify()、notifyAll()三个方法不属于Thread类,而是属于Object类

方法解释:

wait():导致当前线程等待,直到其他线程调用该同步监视器的notify()方法或notifyAll()方法来唤醒该线程。该wait()方法有三种形式-----无时间参数的wait(一直等待,直到其他线程通知)、带毫秒参数的wait()和带毫秒、毫微秒的wait()这两种方法都是等待指定时间后自动苏醒)。调用wait()方法的当前线程会释放对该同步监视器的锁定。

notify():唤醒在此同步监视器上等待的单个线程。如果所有线程都在此同步监视器上等待,则会选择唤醒其中一个线程。选择是任意性的。只有当前线程放弃对该同步监视器的锁定后,才可以执行被唤醒的线程。

notifyAll():唤醒在此同步监视器上等待的所有线程,只有当前线程放弃对该同步监视器的锁定后,才可以执行被唤醒的线程。

3 lock

  • 线程就是一个单独的资源类,没有任何附属的操作。其中有属性,方法,(oop编程,耦合低) 。
  • 传统synchronized:本质就是队列、锁

3.1 Lock三部曲

  1. new ReentrantLock();
  2. lock.lock();//加锁
  3. lock.unlock();//解锁

3.2 Synchronized和Lock的区别

1.Synchronized是内置的java关键字,Lock是一个java接口。

2.Synchronized无法判断是否获取锁,Lock可以判断是否获取到了锁

3.Synchronized会自动释放,Lock必须手动释放锁!如果不释放锁,会产生死锁

4.Synchronized 线程1(获得锁,阻塞)、线程2(等待,傻傻的等);Lock锁就不一定会等待下去;

5.Synchronized是可重入锁,不可以中断的,非公平;Lock,可重入锁,可以中断锁,非公平锁(可以自己设置,在new ReentrantLock后传个true即可)

6.Synchronized适合锁少量的代码同步问题,Lock适合锁大量的同步代码!

3.3 八锁现象

1.不加锁的方法正常执行,加锁的同步方法等待(指睡眠);结果实现执行不加锁的,之后等待完毕再执行同步方法。

2.两个对象调用者有两把锁,锁是不一样的,根据休眠时间的长短,来进行输出,休眠短的先执行。

3.两个静态同步方法在同一个类中:只有一个对象,依据:类一加载就有,Class(模板),static走的是Class对象,锁的是类。两个对象也是一样的,只有一个类模板。

4.在同一个类中,一个普通同步方法和一个静态同步方法,只有一个对象去调用,普通的先调用,普通方法锁的是调用者,静态同步锁的是class类。

5.同一个类。一个同步方法和一个静态同步方法,两个对象,还是同步方法先,静态同步方法后,原因也是锁的不同。

3.4 集合类不安全

3.4.1解决List多线程安全问题

方案一:将ArrayList换成vector安全集合

方案二: 通过工具类Collections转换:Collections.synchronizedList(new ArrayList<>());

方案三:JUC:CopyOnWriteArrayList的setArray()中的array使用了transient和volatile安全修饰的;

3.4.2解决Set多线程安全问题

解决方案一:Collections.synchronizedSet (new HashSet<>());

解决方案二:CopyOnWriteArraySet

3.4.3 阻塞队列(FIFO)

不得不阻塞

  • 写入:如果队列满了。就必须等待
  • 取:如果队列是空的,必须阻塞等待生产

阻塞队列:BlockingQueue(接口) 的父类是Collection和Iterable,和List、Set同级

实现类:ArrayBlockingQueue,LinkedBlockingQueue,SynchronousQueue(同步队列)

4.

4.1 java为何无法开启线程

  • 原因是最终调用了本地方法,底层的c++,java无法直接操作硬件,因为硬件是在虚拟机上进行的

4.2 获取CPU核数

Runtime.getRuntime().availableProcessors()

4.3 并发编程的本质

  • 充分利用CPU的资源
  • 1
    点赞
  • 1
    收藏
    觉得还不错? 一键收藏
  • 2
    评论
评论 2
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

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

抵扣说明:

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

余额充值