线程
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类所具有的优势:
- 适合多个相同的程序代码去共享同一个资源。
- 可以避免java中的单继承的局限性。
- 增加程序的健壮性,实现解耦操作,代码可以被多个线程共享,代码和线程独立。
- 线程池只能放入实现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三部曲
- new ReentrantLock();
- lock.lock();//加锁
- 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的资源