Java和Android开发过程中,我们经常会提到进程、线程,什么是进程,什么是线程?
进程与线程
进程定义:进程是程序运行资源分配的最小单位(资源:CPU、内存空间、IO),进程与进程之间独立。
线程定义:CPU调度的最小单位,必须依赖于进程,一个进程内,允许有多个线程,线程之间可以共享资源。
并行与并发
举个例子,如果有条高速公路 A 上面并排有 8 条车道,那么最大的并行车 辆就是 8 辆此条高速公路 A 同时并排行走的车辆小于等于 8 辆的时候,车辆就可 以并行运行。CPU 也是这个原理,一个 CPU 相当于一个高速公路 A,核心数或者线 程数就相当于并排可以通行的车道;而多个 CPU 就相当于并排有多条高速公路,而 每个高速公路并排有多个车道。
并发:指应用能够交替执行不同的任务,比如单 CPU 核心下执行多线程并非是 同时执行多个任务,如果你开两个线程执行,就是在你几乎不可能察觉到的速度不 断去切换这两个任务,已达到"同时执行效果",其实并不是的,只是计算机的速度太 快,我们无法察觉到而已. 并行:指应用能够同时执行不同的任务,例:吃饭的时候可以边吃饭边打电话, 这两件事情可以同时执行
两者区别:一个是交替执行,一个是同时执行.
Java多线程可以给程序带来如下好处
1.充分利用CPU资源
2.加快响应用户的时间
3.可以使你的代码模块化,异步化,简单化
多线程程序需要注意事项
(1)线程之间的安全性 从前面的章节中我们都知道,在同一个进程里面的多线程是资源共享的,也就 是都可以访问同一个内存地址当中的一个变量。例如:若每个线程中对全局变量、 静态变量只有读操作,而无写操作,一般来说,这个全局变量是线程安全的:若有多 个线程同时执行写操作,一般都需要考虑线程同步,否则就可能影响线程安全。
(2)线程之间的死锁 为了解决线程之间的安全性引入了 Java 的锁机制,而一不小心就会产生 Java 线程死锁的多线程问题,因为不同的线程都在等待那些根本不可能被释放的锁,从
而导致所有的工作都无法完成。假设有两个线程,分别代表两个饥饿的人,他们必 须共享刀叉并轮流吃饭。他们都需要获得两个锁:共享刀和共享叉的锁。
假如线程 A 获得了刀,而线程 B 获得了叉。线程 A 就会进入阻塞状态来等待 获得叉,而线程 B 则阻塞来等待线程 A 所拥有的刀。
(3)线程太多了会将服务器资源耗尽形成死机当机 线程数太多有可能造成系统创建大量线程而导致消耗完系统内存以及 CPU 的“过渡切换”,造成系统的死机,那么我们该如何解决这类问题呢? 某些系统资源是有限的,如文件描述符。多线程程序可能耗尽资源,因为每个 线程都可能希望有一个这样的资源。如果线程数相当大,或者某个资源的侯选线 程数远远超过了可用的资源数则最好使用资源池。一个最好的示例是数据库连接 池。只要线程需要使用一个数据库连接,它就从池中取出一个,使用以后再将它返 回池中。资源池也称为资源库。
面试中可能会被问到,Java新启线程的方式有几种?
从Java源码上,我们可以看到Java新启线程的方式有两种。
There are two ways to create a new thread of execution. One is to declare a class to be a subclass of Thread.
This subclass should override the run method of class Thread.
An instance of the subclass can then be allocated and started.
For example, a thread that computes primes larger than a stated value could be written as follows:
从Thread源码注释上,我们可以看到,Java启动线程的方式有两种:
继承Thread类,重写run()方法
public class StartThreadFirstMethod {
static class FirstMethodThread extends Thread{
@Override
public void run() {
System.out.println("启动线程第一种方法,继承Thread");
}
}
public static void main(String[] args) {
FirstMethodThread firstMethodThread=new FirstMethodThread();
firstMethodThread.start();
}
}
实现Runnable接口,实现run方法
public class StartThreadSecondMethod {
static class PrimeRun implements Runnable{
@Override
public void run() {
System.out.println("启动线程第二种方法,implements Runnable");
}
}
public static void main(String[] args) {
PrimeRun primeRun=new PrimeRun();
new Thread(primeRun).start();
}
}
Thread类是Java对线程概念的抽象,new Thread(),只是new出一个Thread的实例,还没有和操作系统中真正的线程挂钩,只有执行了start()方法,才真正意义上启动了线程。start()方法让线程进入就绪状态,等待分配CPU,分到CPU以后才会执行run()方法,start()方法不能重复调用,重复调用会抛异常!!而 run 方法是业务逻辑实现的地方,本质上和任意一个类的任意一个成员方法并没有任何区别,可以重复执行,也可以被单独调用。
有开始就有结束,怎么样才能让Java里的线程安全停止工作呢?
1.线程自然中止
1).run执行完成;
2).抛出一个未处理的异常,导致线程提前结束
2.stop()
暂停、恢复和停止操作对应在线程 Thread 的 API 就是 suspend()、resume() 和 stop()。但是这些 API 是过期的,也就是不建议使用的。不建议使用的原因主 要有:以 suspend()方法为例,在调用后,线程不会释放已经占有的资源(比如 锁),而是占有着资源进入睡眠状态,这样容易引发死锁问题。同样,stop()方 法在终结一个线程时不会保证线程的资源正常释放,通常是没有给予线程完成资 源释放工作的机会,因此会导致程序可能工作在不确定状态下。正因为 suspend()、 resume()和 stop()方法带来的副作用,这些方法才被标注为不建议使用的过期方法。
3.interrupt()
安全的中止则是其他线程通过调用某个线程 A 的 interrupt()方法对其进行中 断操作, 中断好比其他线程对该线程打了个招呼,“A,你要中断了”,不代表 线程 A 会立即停止自己的工作,同样的 A 线程完全可以不理会这种中断请求。 因为 java 里的线程是协作式的,不是抢占式的。线程通过检查自身的中断标志 位是否被置为 true 来进行响应。
线程通过方法 isInterrupted()来进行判断是否被中断,也可以调用静态方法 Thread.interrupted()来进行判断当前线程是否被中断,不过 Thread.interrupted() 会同时将中断标识位改写为 false。
public class StartThreadFirstMethod {
static class FirstMethodThread extends Thread {
public FirstMethodThread(String firstMethodThread) {
super(firstMethodThread);
}
@Override
public void run() {
String name = Thread.currentThread().getName();
System.out.println(name + " interrupte flag = " + isInterrupted());
while (!isInterrupted()) {
System.out.println(name + " is runing");
System.out.println(name + " inner interrupte flag = " + isInterrupted());
}
System.out.println(name + " end interrupte flag = " + isInterrupted());
}
}
public static void main(String[] args) throws InterruptedException {
FirstMethodThread firstMethodThread = new FirstMethodThread("FirstMethodThread");
firstMethodThread.start();
Thread.sleep(30);
firstMethodThread.interrupt();
}
}
FirstMethodThread is runing
FirstMethodThread inner interrupte flag = false
FirstMethodThread is runing
FirstMethodThread inner interrupte flag = true
FirstMethodThread end interrupte flag = true
从输出结果可以看到,调用isInterrupted()方法,未改变中断标识位。
调用Thread.interrupted()方法,我们可以看到中断标识位改写为 false。
public class StartThreadFirstMethod {
static class FirstMethodThread extends Thread {
public FirstMethodThread(String firstMethodThread) {
super(firstMethodThread);
}
@Override
public void run() {
String name = Thread.currentThread().getName();
System.out.println(name + " interrupte flag = " + isInterrupted());
while (!Thread.interrupted()) {
System.out.println(name + " is runing");
System.out.println(name + " inner interrupte flag = " + isInterrupted());
}
System.out.println(name + " end interrupte flag = " + isInterrupted());
}
}
public static void main(String[] args) throws InterruptedException {
FirstMethodThread firstMethodThread = new FirstMethodThread("FirstMethodThread");
firstMethodThread.start();
Thread.sleep(5);
firstMethodThread.interrupt();
}
}
FirstMethodThread is runing
FirstMethodThread inner interrupte flag = false
FirstMethodThread is runing
FirstMethodThread inner interrupte flag = false
FirstMethodThread is runing
FirstMethodThread inner interrupte flag = true
FirstMethodThread end interrupte flag = false
如果一个线程处于了阻塞状态(如线程调用了 thread.sleep、thread.join、 thread.wait 等),则在线程在检查中断标示时如果发现中断标示为 true,则会在 这些阻塞方法调用处抛出 InterruptedException 异常,并且在抛出异常后会立即 将线程的中断标示位清除,即重新设置为 false。
public class StartThreadFirstMethod {
static class FirstMethodThread extends Thread {
public FirstMethodThread(String firstMethodThread) {
super(firstMethodThread);
}
@Override
public void run() {
String threadName = currentThread().getName();
System.out.println("thread name is "+threadName);
while (!isInterrupted()){
try {
sleep(100);
System.out.println(threadName +" is running");
} catch (InterruptedException e) {
e.printStackTrace();
// interrupt();
System.out.println(threadName +" isInterrupted is "+isInterrupted());
}
}
System.out.println(threadName +" end isInterrupted is "+isInterrupted());
}
}
public static void main(String[] args) throws InterruptedException {
FirstMethodThread firstMethodThread = new FirstMethodThread("FirstMethodThread");
firstMethodThread.start();
Thread.sleep(1000);
firstMethodThread.interrupt();
}
}
FirstMethodThread is running
FirstMethodThread is running
FirstMethodThread is running
FirstMethodThread is running
FirstMethodThread is running
FirstMethodThread is running
FirstMethodThread is running
java.lang.InterruptedException: sleep interrupted
at java.lang.Thread.sleep(Native Method)
at com.java.base.thread.StartThreadFirstMethod$FirstMethodThread.run(StartThreadFirstMethod.java:15)
FirstMethodThread isInterrupted is false
FirstMethodThread is running
从输出结果,可以看到,虽然调用了线程的interrupt方法,但是线程中断标识位打印出来仍然是false,这是因为在抛出中断异常的异常时,中断标识也同时被修改成了false,所以线程不会中止,仍然会一直输出内容,只有在捕获异常的位置再次调用interrupt方法,线程才会被真正的中断。
public class StartThreadFirstMethod {
static class FirstMethodThread extends Thread {
public FirstMethodThread(String firstMethodThread) {
super(firstMethodThread);
}
@Override
public void run() {
String threadName = currentThread().getName();
System.out.println("thread name is "+threadName);
while (!isInterrupted()){
try {
sleep(100);
System.out.println(threadName +" is running");
} catch (InterruptedException e) {
e.printStackTrace();
interrupt();
System.out.println(threadName +" isInterrupted is "+isInterrupted());
}
}
System.out.println(threadName +" end isInterrupted is "+isInterrupted());
}
}
public static void main(String[] args) throws InterruptedException {
FirstMethodThread firstMethodThread = new FirstMethodThread("FirstMethodThread");
firstMethodThread.start();
Thread.sleep(1000);
firstMethodThread.interrupt();
}
}
FirstMethodThread is running
FirstMethodThread is running
java.lang.InterruptedException: sleep interrupted
at java.lang.Thread.sleep(Native Method)
at com.java.base.thread.StartThreadFirstMethod$FirstMethodThread.run(StartThreadFirstMethod.java:15)
FirstMethodThread isInterrupted is true
FirstMethodThread end isInterrupted is true
捕获异常的位置再次调用interrupt方法,线程才会被真正的中断,中断标识被设置为true.
其他线程相关方法
1.yield()
使当前CPU让出CPU占有权,但让出的时间是不可设定的,也不会释放锁资源。线程进入就绪状态,再次被操作系统选中时又重新开始执行。
线程生命周期
2.join()
把指定的线程加入到当前线程,可以将两个交替执行的线程合并为顺序执行。 比如在线程 B 中调用了线程 A 的 Join()方法,直到线程 A 执行完毕后,才会继续 执行线程 B。
面试考点:如何让两个线程顺序执行
/**
*类说明:演示Join()方法的使用
*/
public class UseJoin {
static class Goddess implements Runnable {
private Thread thread;
public Goddess(Thread thread) {
this.thread = thread;
}
public Goddess() {
}
public void run() {
System.out.println("Goddess开始排队打饭.....");
try {
if(thread!=null) thread.join();
} catch (InterruptedException e) {
}
SleepTools.second(2);//休眠2秒
System.out.println(Thread.currentThread().getName()
+ " Goddess打饭完成.");
}
}
static class GoddessLikefriend implements Runnable {
public void run() {
SleepTools.second(2);//休眠2秒
System.out.println("Goddess暗恋目标开始排队打饭.....");
System.out.println(Thread.currentThread().getName()
+ " Goddess暗恋目标打饭完成.");
}
}
public static void main(String[] args) throws Exception {
Thread beiTaiThread = Thread.currentThread();
GoddessLikefriend goddessLikefriend = new GoddessLikefriend();
Thread gbf = new Thread(goddessLikefriend);
Goddess goddess = new Goddess(gbf);
//Goddess goddess = new Goddess();
Thread g = new Thread(goddess);
g.start();
gbf.start();
System.out.println("备胎开始排队打饭.....");
g.join();
SleepTools.second(2);//让主线程休眠2秒
System.out.println(Thread.currentThread().getName() + " 备胎打饭完成.");
}
}
SleepTools工具类代码如下
public class SleepTools {
/**
* 按秒休眠
* @param seconds 秒数
*/
public static final void second(int seconds) {
try {
TimeUnit.SECONDS.sleep(seconds);
} catch (InterruptedException e) {
}
}
/**
* 按毫秒数休眠
* @param seconds 毫秒数
*/
public static final void ms(int seconds) {
try {
TimeUnit.MILLISECONDS.sleep(seconds);
} catch (InterruptedException e) {
}
}
}
输出结果:
Goddess开始排队打饭.....
备胎开始排队打饭.....
Goddess暗恋目标开始排队打饭.....
Thread-0 Goddess暗恋目标打饭完成.
Thread-1 Goddess打饭完成.
main 备胎打饭完成.
3.setPriority(int)
在 Java 线程中,通过一个整型成员变量 priority 来控制优先级,优先级的范 围从 1~10,在线程构建的时候可以通过 setPriority(int)方法来修改优先级,默认 优先级是 5,优先级高的线程分配时间片的数量要多于优先级低的线程。 设置线程优先级时,针对频繁阻塞(休眠或者 I/O 操作)的线程需要设置较 高优先级,而偏重计算(需要较多 CPU 时间或者偏运算)的线程则设置较低的 优先级,确保处理器不会被独占。在不同的 JVM 以及操作系统上,线程规划会 存在差异,有些操作系统甚至会忽略对线程优先级的设定。
4.setDaemon(true)
Daemon(守护)线程是一种支持型线程,因为它主要被用作程序中后台调 度以及支持性工作。这意味着,当一个 Java 虚拟机中不存在非 Daemon 线程的 时候,Java 虚拟机将会退出。可以通过调用 Thread.setDaemon(true)将线程设置 为 Daemon 线程。我们一般用不上,比如垃圾回收线程就是 Daemon 线程。
Daemon 线程被用作完成支持性工作,但是在 Java 虚拟机退出时 Daemon 线 程中的 finally 块并不一定会执行。在构建 Daemon 线程时,不能依靠 finally 块中 的内容来确保执行关闭或清理资源的逻辑。
public class DaemonThread {
private static class UseThread extends Thread{
@Override
public void run() {
try {
while (!isInterrupted()) {
System.out.println(Thread.currentThread().getName()
+ " I am extends Thread.");
}
System.out.println(Thread.currentThread().getName()
+ " interrupt flag is " + isInterrupted());
} finally {
//守护线程中finally不一定起作用
System.out.println(" .............finally");
}
}
}
public static void main(String[] args)
throws InterruptedException{
UseThread useThread = new UseThread();
useThread.setDaemon(true);
useThread.start();
Thread.sleep(5);
// useThread.interrupt();
}
}
当用户线程结束时,守护线程也同时结束,finally不一定会执行,用户线程里的finally肯定会执行,只有线程为守护线程的时候,finally可能不会执行。
5.sleep()
面试中经常会被问到sleep方法对锁的影响,用代码验证一下
public class SleepLock {
private Object lock = new Object();
public static void main(String[] args) {
SleepLock sleepTest = new SleepLock();
Thread threadA = sleepTest.new ThreadSleep();
threadA.setName("ThreadSleep");
Thread threadB = sleepTest.new ThreadNotSleep();
threadB.setName("ThreadNotSleep");
threadA.start();
try {
Thread.sleep(1000);
System.out.println(" Main slept!");
} catch (InterruptedException e) {
e.printStackTrace();
}
threadB.start();
}
private class ThreadSleep extends Thread{
@Override
public void run() {
String threadName = Thread.currentThread().getName();
System.out.println(threadName+" will take the lock");
try {
synchronized(lock) {
System.out.println(threadName+" taking the lock");
Thread.sleep(5000);
System.out.println("Finish the work: "+threadName);
}
} catch (InterruptedException e) {
//e.printStackTrace();
}
}
}
private class ThreadNotSleep extends Thread{
@Override
public void run() {
String threadName = Thread.currentThread().getName();
System.out.println(threadName+" will take the lock time="+System.currentTimeMillis());
synchronized(lock) {
System.out.println(threadName+" taking the lock time="+System.currentTimeMillis());
System.out.println("Finish the work: "+threadName);
}
}
}
}
ThreadSleep will take the lock
ThreadSleep taking the lock
Main slept!
ThreadNotSleep will take the lock time=1606999948908
Finish the work: ThreadSleep
ThreadNotSleep taking the lock time=1606999952908
Finish the work: ThreadNotSleep
从输出结果看,调用了sleep方法以后,锁并没有释放
线程间的共享和协作
1.synchronized
Java 支持多个线程同时访问一个对象或者对象的成员变量,关键字 synchronized 可以修饰方法或者以同步块的形式来进行使用,它主要确保多个线 程在同一个时刻,只能有一个线程处于方法或者同步块中,它保证了线程对变量访问的可见性和排他性,又称为内置锁机制。
对象锁和类锁: 对象锁是用于对象实例方法,或者一个对象实例上的,类锁是用于类的静态 方法或者一个类的 class 对象上的。我们知道,类的对象实例可以有很多个,但 是每个类只有一个 class 对象,所以不同对象实例的对象锁是互不干扰的,但是 每个类只有一个类锁。
不加锁,可能造成的现象,例如
public class SynTest {
private long count =0;
private Object obj = new Object();//作为一个锁
public long getCount() {
return count;
}
public void setCount(long count) {
this.count = count;
}
/*用在同步块上*/
public void incCount(){
// synchronized (obj){
count++;
// }
}
/*用在方法上*/
public synchronized void incCount2(){
count++;
}
/*用在同步块上,但是锁的是当前类的对象实例*/
public void incCount3(){
synchronized (this){
count++;
}
}
//线程
private static class Count extends Thread{
private SynTest simplOper;
public Count(SynTest simplOper) {
this.simplOper = simplOper;
}
@Override
public void run() {
for(int i=0;i<10000;i++){
simplOper.incCount();//count = count+10000
}
}
}
public static void main(String[] args) throws InterruptedException {
SynTest simplOper = new SynTest();
//启动两个线程
Count count1 = new Count(simplOper);
Count count2 = new Count(simplOper);
count1.start();
count2.start();
Thread.sleep(50);
System.out.println(simplOper.count);//20000
}
}
启动两个线程,分别对simplOper对象中count值进行10000次count++操作,理论上我们最后输出的数值应该为20000,但是实际运行结果中,只会偶尔输出20000的结果。
加锁以后,我们就会得到我们想要的值了
public class SynTest {
private long count =0;
private Object obj = new Object();//作为一个锁
public long getCount() {
return count;
}
public void setCount(long count) {
this.count = count;
}
/*用在同步块上*/
public void incCount(){
synchronized (obj){
count++;
}
}
/*用在方法上*/
public synchronized void incCount2(){
count++;
}
/*用在同步块上,但是锁的是当前类的对象实例*/
public void incCount3(){
synchronized (this){
count++;
}
}
//线程
private static class Count extends Thread{
private SynTest simplOper;
public Count(SynTest simplOper) {
this.simplOper = simplOper;
}
@Override
public void run() {
for(int i=0;i<10000;i++){
simplOper.incCount();//count = count+10000
}
}
}
public static void main(String[] args) throws InterruptedException {
SynTest simplOper = new SynTest();
//启动两个线程
Count count1 = new Count(simplOper);
Count count2 = new Count(simplOper);
count1.start();
count2.start();
Thread.sleep(50);
System.out.println(simplOper.count);//20000
}
}
当然我们也可以使用incCount2方法或者incCount3方法,incCount2为同步方法,incCount3为对象锁,都会达到加锁的效果
错误的加锁方式,例如
public class TestIntegerSyn {
public static void main(String[] args) throws InterruptedException {
Worker worker=new Worker(1);
//Thread.sleep(50);
for(int i=0;i<5;i++) {
new Thread(worker).start();
}
}
private static class Worker implements Runnable{
private Integer i;
private Object o = new Object();
public Worker(Integer i) {
this.i=i;
}
@Override
public void run() {
synchronized (i) {
Thread thread=Thread.currentThread();
System.out.println(thread.getName()+"--@"
+System.identityHashCode(i));
i++;
System.out.println(thread.getName()+"-------"+i+"-@"
+System.identityHashCode(i));
try {
Thread.sleep(1000);
} catch (InterruptedException e) {
e.printStackTrace();
}
System.out.println(thread.getName()+"-------"+i+"--@"
+System.identityHashCode(i));
}
}
}
}
输出结果
Thread-0--@1096443612
Thread-0-------2-@1335726215
Thread-4--@1335726215
Thread-4-------3-@357858746
Thread-0-------3--@357858746
Thread-4-------3--@357858746
Thread-3--@357858746
Thread-3-------4-@1892057330
Thread-3-------4--@1892057330
Thread-2--@1892057330
Thread-2-------5-@1488708714
Thread-2-------5--@1488708714
Thread-1--@1488708714
Thread-1-------6-@390233048
Thread-1-------6--@390233048
从输出结果看,Thread-3--@357858746的位置输出的不对,可见我们加的锁并没有起到作用,原因是在执行i++的过程中,i的对象已经改变,我们可以反编译一下class文件可以看到,每次执行i++都是调用的Integer的valueOf方法,而valueOf方法是new 一个新的对象,锁的对象发生了改变,这是一个错误的加锁方式。
2.volatile 最轻量的同步机制
volatile 保证了不同线程对这个变量进行操作时的可见性,即一个线程修改了某个变量的值,这新值对其他线程来说是立即可见的,但是volatile不能保证操作的原子性。
volatile 最适用的场景:一个线程写,多个线程读