线程在面试中已经是常客了,也是我们必备的知识点,关于线程,问的最多的便是线程是什么?为什么使用多线程?多线程的示例以及解决方案?线程池是什么?
一.线程是什么?
java.lang.Thread类中有这样的明确定义:线程是程序中执行的线程,Java虚拟机允许程序同时运行多个执行线程。
1、线程有6种状态:新建,运行(可运行),阻塞,等待,计时等待和终止。
在给定的时间点,线程只能处于一种状态,查看运行状态:Thread类的getState()方法。
Thread.State状态枚举类:
public enum State {
/**
* 尚未启动,新建
* 当使用new操作符创建新线程时,线程处于“新建”状态
*/
NEW, <span >/**
* 可运行,正在Java虚拟机中执行,但是它可能正在等待来自操作系统的其他资源
* 调用start()方法
*/</span>
RUNNABLE<span >,</span>
<span >/**
* 阻塞,当线程需要获得对象的内置锁,而该锁正在被其他线程拥有
* 正在等待监视器锁定以synchronized同步方法/块,或者调用同步方法/块后重新进入wait状态
*/</span>
BLOCKED<span >,</span>
<span >/**
* 等待,调用以下方法之一,使处于等待状态
* Object.wait 没有超时
* Thread.join 没有超时
* LockSupport.park
*
* 处于等待状态的线程正在等待另一个线程执行特定操作。
* 例如一个已调用wait()方法正在等待另一个线程来呼叫notify()/notifyAll()
* 被调用的线程Thread.join正在等待指定的线程终止
*/</span>
WAITING<span >,</span>
<span >/**
* 计时等待,具有指定等待时间的等待线程的线程状态,调用以下方法之一,使其处于定时等待状态
* Thread.sleep
* Object.wait 随着超时
* Thread.join 随着超时
* LockSupport.parkNanos
* LockSupport.parkUntil
*
*/</span>
TIMED_WAITING<span >,</span>
<span >/**
* 终止,当run方法运行完毕或出现异常时.
*/</span>
TERMINATED<span >;</span>
<span >}</span>
1)编写类ThreadState,实现Runnable接口
public class ThreadState implements Runnable {
public synchronized void waitForASecond() throws InterruptedException {
wait(500);//将当前线程暂时等待0.5秒或其他线程调用notify()或notifyAll()
}
public synchronized void waitForLong() throws InterruptedException {
wait();//永久等待,直到其他线程调用notify()或notifyAll()
}
public synchronized void notifyNow() throws InterruptedException {
notify();//唤醒由调用wait()方法进入等待状态的线程
}
@Override
public void run() {
try {
waitForASecond();//在新线程中运行waitForASecond()方法
waitForLong();//在新线程中运行waitForLong()
} catch (InterruptedException e) {
e.printStackTrace();
}
}
}
2)测试类:ThreadTest
public class ThreadTest {
public static void main(String[] args) throws InterruptedException {
ThreadState state=new ThreadState();
Thread thread=new Thread(state);//创建Thread对象
System.out.println("新建线程:"+thread.getState());
thread.start();
System.out.println("启动线程:"+thread.getState());
Thread.sleep(100);//当前线程休眠,时新线程运行waitForASecond()方法
System.out.println("计时等待:"+thread.getState());
Thread.sleep(1000);//当前线程休眠,时新线程运行waitForLong()方法
System.out.println("等待线程:"+thread.getState());
state.notifyNow();
System.out.println("唤醒线程:"+thread.getState());
Thread.sleep(1000);//当前线程休眠,时新线程结束
System.out.println("终止线程:"+thread.getState());
}
}
运行结果:
2.Java中每个线程都有优先级属性,具有较高优先级的线程优先于优先级较低的线程执行,每个线程可能也可能不会被标记为守护进程。当在某个线程中运行创建一个新的 Thread对象时,新的线程的优先级最初设置为等于创建线程的优先级,并且当且仅当创建线程是守护进程时才是守护进程线程。
(线程的优先级用数字来表示,范围从1~10,主线程的默认优先级为5 :
Thread.MIN_PRIORITY=1;Thread.MAX_PRIORITY=10;Thread.NORM_PRIORITY=5
当Java虚拟机启动时,通常会有一个非守护程序线程(通常调用某个指定类的名为 main的方法)
Java虚拟机继续执行线程直到发生以下任一情况:
- 已调用类 Runtime的exit方法,并且安全管理器已允许执行退出操作。
- 所有非守护程序线程的线程都已经死亡,要么通过调用返回run方法,要么抛出一个超出run 方法传播的异常。
问:线程与进程?:进程是CPU,内存等资源占用的基本单位,线程是不能独立占有这些资源的;进程之间相互独立,通信比较困难,线程之间共享一块内存区域,通信方便
二.怎么创建一个线程呢?
Thread中规定:有两种方法可以创建新的执行线程。
-
声明一个类是Thread类,该子类还应该覆盖Thread类中的run方法,然后可以分配并启动子类的示例。
class MyThread extends Thread {
@Override
public void run() {
//具体的方法
System.out.println(Calendar.getInstance().getTime());//系统时间
}
}
然后创建一个线程并开始运行:
MyThread myThread=new MyThread();
myThread.start();
- 声明一个实现 Runnable接口的类, 该类然后实现 run 方法, 然后可以分配类的实例,在创建 Thread时作为参数传递,然后启动。
class MyThreard implements Runnable {
public void run() {
System.out.println(Calendar.getInstance().getTime());
}
}
然后创建一个线程并开始运行:
MyThreard p = new MyThreard();
new Thread(p).start();
三.为什么使用多线程?
多线程指的是在单个程序中可以同时运行多个不同的线程,执行不同的任务
- 更高的运行效率,——并行;
- 多线程是模块化的编程模型;
- 与进程相比,线程的创建和切换开销更小;
- 通信方便;
- 能简化程序的结构,便于理解和维护;更高的资源利用率。
注:多线程上下文切换的性能损耗:上下文切换(线程切换,进程切换,模式切换,地址空间切换)——中断处理(硬件中断,软件中断—线程被挂起);多任务处理(每个程序都有相应的处理时间片);用户态切换。
示例:
-
利用多线程技术模拟出龟兔赛跑的场面,设计一个线程类模拟参与赛跑的角色,创建该类的两个对象分别代表乌龟和兔子,让兔子跑快些,但在路上睡眠休息时间长些,到終点时线程运行结束。
思路:
1.利用多线程,根据题意则需要设计两个线程类——兔子(Rabbit),乌龟(Tortoise),抢占式, sleep()方法
2.赛跑则需要赛程,选手,选手速度,以及比赛结果——添加属性,速度(speed),赛程(distance),结果(winner)
3.兔子在中途会休息,因此需要在中途让兔子休息——sleep()长时间
4.一个裁判——测试类,让龟兔赛跑
TestThread:测试类
龟兔线程类:
运行结果:
四.多线程的安全问题
简单测试你的线程是否安全:如果你的代码在多线程下执行和在单线程下执行永远都能获得一样的结果,那么你的代码就是线程安全的。
如果不一致,那就可能是多线程的资源安全有问题,常见的情况如下:
1、临界资源问题:多个线程同时访问相同的资源并进行读写操作
解决思路:避免竞态条件,多线程同步,必须获得每一个线程对象的锁(lock)
示例代码(一个线程进行加操作,一个线程进行减操作):
public class Stack {
int idx=0;
char[] data=new char[10];
public void push(char c) {
synchronized (this) {//在执行该代码段时必须取得对象锁
data[idx]=c;
idx++; }
}
public synchronized char pop() {//在执行该方法时必须取得对象锁
idx--;
return data[idx];
}
}
注:之所以用synchronized就能解决困难的同步问题,与Java的内置锁密切相关,从1.0版本开始,每个Java对象都有一个内置锁。如果方法用synchronized 关键字声明,内置锁会保护整个方法。即在调用该方法前,需要获得内置锁,否则就处于阻塞状态。
Java提供了很多方式和工具类来帮助程序员简化多线程的开发,同步方法是最简单和常用的一种方法。
同步实现的方法:(这里上一张百度的图,个人觉得总结的还是OK的)
注:另外还可以使用volatile关键字实现同步,volatile关键字为域变量的访问提供了一种免锁机制。使用volatile修饰域相当于告诉虚拟机该域可能被其他线程更新,因此每次使用该域就要重新计算,而不是使用寄存器中的值,volatile不会提供任何原子操作,也不能用来修饰final类型的变量。生产消费者模式:一个线程负责生产数据,放到共享区域,然后通知另一个线程去消耗数据。
(1)用wait()和notify():这样消费者线程就不用不停去检查是否有数据被产生。
(2)用阻塞队列实现:BlockingQueue中提供了put()和take()方法,可以极大简化生产者消费者模式的实现过程。这一过程的基本原理是,如果队列满了,put()方法就会被阻塞;如果队列是空的,take()方法会阻塞。与传统的wait()和notify()方法相比,使用阻塞队列更简单,更便于理解。
2.死锁:死锁也是一种因为对资源争夺而出现的状态,是指两个或两个以上的进程在执行过程中,因争夺资源而造成的一种互相等待的现象,若无外力作用,它们将一直互相等待而无法推进下去。不断地加锁,锁中锁,锁套锁,最后造成循环,就可能会成为死锁;因此要解决死锁,尽量避免对同一资源的剥夺,请求和保持,避免循环等待。
五.线程池
目的是减小对象的创建和注销的开支,减轻JVM的压力。
1)避免线程的创建和销毁带来的性能开销。
2)避免大量的线程间因互相抢占系统资源导致的阻塞现象。
3}能够对线程进行简单的管理并提供定时执行、间隔执行等功能。
主要核心概念有四个:
ThreadPool–>线程池管理器:用于创建并管理线程池(创建线程池,销毁线程池,添加新任务);
PoolWorker–>工作线程
Task–>任务接口,每个接口必须实现的接口
TaskQue–>任务队列,用于存放没有处理的任务;提供一种缓冲机制
例:使用new操作符创建大量线程和使用线程池创建对比
TempThread类:
public class TempThread implements Runnable{
private int id=0;
@Override
public void run() {
id++;
}
}
ThreadPoolTest类:
public class ThreadPoolTest {
public static void main(String[] args) {
Runtime run = Runtime.getRuntime();//创建Runtime对象
run.gc();//运行垃圾回收器
long freeMemory=run.freeMemory();//获得当前虚拟机的空闲内存
long currentTime=System.currentTimeMillis();//获得当前虚拟机的时间
for (int i=0;i<100;i++){//独立运行1000个线程
new Thread(new TempThread()).start();
}
//查看内存变化
System.out.println("独立运行1000个线程所占用的内存:"+(freeMemory-run.freeMemory()));
System.out.println("独立运行1000个线程所消耗时间:"+(System.currentTimeMillis()-currentTime));
run.gc();//运行垃圾回收器
freeMemory=run.freeMemory();//获得当前虚拟机的空闲内存
currentTime=System.currentTimeMillis();//获得当前虚拟机的时间
ExecutorService executorService= Executors.newFixedThreadPool(2);//创建线程池
for (int i=0;i<100;i++){//独立运行1000个线程
executorService.submit(new TempThread());
}
System.out.println("独立运行1000个线程所占用的内存:"+(freeMemory-run.freeMemory()));
System.out.println("独立运行1000个线程所消耗时间:"+(System.currentTimeMillis()-currentTime));
}
}
运行结果: