线程的生命周期
1.新建 :当一个Thread类或其子类的对象被生命并创建时,新生的线程对象处于新建状态。
2.就绪 :处于新建状态的线程被start()后,将进入线程队列等待CPU时间片,此时它已具备了运行的条件,只是没分配到CPU资源
3.运行 :当就绪的线程被调度并获得CPU资源时,便进入运行状态,run()方法定义了线程的操作和功能。
4.阻塞 :在某种特殊情况下,被人为挂起或执行输入输出操作时,让出CPU并临时中止自己运行的状态,进入阻塞状态。
5.死亡 :线程完全了它的全部工作或线程被提前强制性地中止或出现异常导致结束。
线程的同步—安全问题
经典例子: 两个人同时对同一个银行账户进行操作, 账户余额5k,a取3k,b取3k。结果都同时操作完成, 账户余额变成了-1k。
下面对售票系统的诉说。
在添加一个synchronized锁之后,能解决重票的问题,synchronized给线程加上了一个锁,其他线程没拿到就不能执行逻辑代码, 等待当前拿到锁的线程处理完逻辑释放锁之后被下一个线程拿到,下一个线程的逻辑才能接着运行。
这种方式叫做:同步代码块。
synchronized(同步监视器--条件){
//需要被同步的代码
}
说明:
1.操作共享数据的代码,即为需要被同步的代码。
2.共享数据:多个线程共同操作的变量,例如上面例子的票数。
3.同步监视器。俗称:锁,任何一个类的对象,都可以充当锁。要求:多个线程必须要共用同一把锁。
第二种方法:同步方法
如果操作共享数据的代码完整的声明在一个方法中, 我们就声明该方法为同步方法
在方法中加synchronized的修饰词。
--要观察好代码运行的逻辑,决定是否需要将逻辑块再次单独封装成一个方法,并在当前方法前面加synchronized修饰。
解决继承Thread的线程问题:
private static synchronized void reduce(){//static == 指示当前类 在加synchronized中
if(ticket > 0){
System.out.println(Thread.currentThread().getName() + "抢到锁,进行消耗: " + ticket);
ticket --;
}
}
第三种方法:Lock锁—JDK5.0开始
eg:
//1.实例化
private ReentrantLock lock = new ReentrantLock();
@Override
public void run() {
while (true){
try{
//调用lock()
lock.lock();
if(ticket > 0){
try {
Thread.sleep(100);
} catch (InterruptedException e) {
e.printStackTrace();
}
System.out.println(Thread.currentThread().getName() + ":售票。票号:" + ticket);
ticket --;
}else{
break;
}
}finally {
//3.调用解锁的方法
lock.unlock();
}
}
}
同步的好处:解决了线程的安全问题, ----好处
操作同步代码时只能有一个线程参与,其他线程需要等待,–相当于单线程执行,效率低。
synchronized用法— 必须是线程中共享的唯一变量,否则的话 线程安全问题仍然会产生,synchronized也没用。
synchronized跟Lock的异同?
同:都可以解决线程的安全问题。
不同:synchronized机制在执行完相应的同步代码后,自动释放同步监视器。
而Lock在语句结束需要调用unlock方法。
死锁
- 不同的线程分别占用对方需要的同步资源不放弃,都在等待对方放弃自己需要的同步资源,就会形成了线程的死锁
- 出现死锁后,不会出现异常,不会出现提示,只是所有的线程都处于阻塞状态。
解决方法:
- 专门的算法、原则
- 尽量减少同步资源的定义
- 尽量减免嵌套同步
线程三个方法:
wait() —一旦执行此方法,线程就会进入到阻塞状态,并且释放同步监视器。
notify() — 一旦调用此方法, 就会唤醒调用了wait方法的线程。如果有多个线程被wait,就唤醒优先级高的线程。
notifyAll() —一旦执行此方法,就会唤醒所有被wait的线程。
上述的三个方法,必须使用在同步代码块或者同步方法中的同步监视器。
sleep()和wait()的异同:
1.都是使线程进入阻塞状态。
2.不同:wait()需要等待线程调用notify()或者notifyAll(),
sleep()只需要等待时间过去就会唤醒线程。
两个方法声明的位置不同:Thread类中声明Sleep(),Object类中声明wait()。
sleep()可以在任何需要调用的地方调用, 而wait()需要在同步块内。
如果两个方法都在同步内容内,sleep()不会释放锁,wait()会释放锁。
JDK5新增:Callable接口
Callable接口功能更强大:
相比Run()方法,Callable可以有返回值。
方法可以抛出异常。
支持泛型的返回值。
需要借助FutureTask类,比如获取返回结果。
Future接口:
可以对具体Runnable、Callable任务的执行结果进行取消、查询是否完成、获取结果等。
FutureTask是Futrue接口的唯一的实现类。
FutureTask同时实现了Runnable,Future接口。它3既可以作为Runnable被线程执行,又可以作为Future得到Callable的返回值。
具体用例:
class NumThread implements Callable {
@Override
public Object call() throws Exception {
int sum = 0;
for(int i = 1 ; i <= 100 ; i++){
if(i % 2 == 0){
System.out.println(i);
sum += i;
}
}
return sum;
}
}
public class ThreadNewCallable {
public static void main(String[] args) {
NumThread numThread = new NumThread();
FutureTask futureTask = new FutureTask<>(numThread);
new Thread(futureTask).start();
try {
//get()返回值即为FutureTask构造器参数Callable实现 类重写的call()的返回值
Object object = futureTask.get();
System.out.println(object);
} catch (InterruptedException e) {
e.printStackTrace();
} catch (ExecutionException e) {
e.printStackTrace();
}
}
}
上面代码实现步骤:
1.创建一个实现Callable的实现类
2.实现call方法,将此线程需要执行的操作声明在call()中,这个call方法是拥有返回值的
3.创建Callable接口实现类的对象
4.将此Callable接口实现类的对象作为参数传递到FutureTask构造器中,创建FutureTask的对象
5.将FutureTask的对象作为参数传递到Thread类的构造器中,创建Thread中并且调用start()
如果要获取call()的返回值,调用FutureTask.get()就可以了。
如何理解实现Callable接口的方式创建多线程比实现Runnable接口创建多线程方式强大?
1.call()拥有返回值。
2.call()可以抛出异常,根据异常做处理。被外面的操作捕获,获取异常信息
3.Callable是支持泛型的
线程池:
背景:经常创建和销毁、使用量特别大的资源,比如并发情况下的线程,对性能影响很大。
思路:提前创建好多个线程,放入线程池中,使用时直接获取, 使用完放回池中。可以避免频繁创建销毁、实现重复利用。
好处:
1.提高响应速度(减少了创建新线程的时间)
2.降低资源消耗(重复利用线程池中线程,不需要每次都创建)
3.便于线程管理:
corePoolSize:核心池的大小
maximumPoolSize:最大线程数
KeepAliveTime:线程没有任务时最多保持多长时间后会终止。
JDK5.0提供了线程池相关的API:ExecutorService和Executors
ExecutorService: 真正的线程池接口。常见子类ThreadPoolExecutor
void execute(Runnable command):执行任务/命令,没有返回值,一般用来执行Runnable
Future submit(Callable task):执行任务,有返回值,一般用来执行Callable
void shutdown():关闭连接池。
Executors: 工具类、线程池的工厂类,用于创建并返回不同类型的线程池
Executors.newCachedThreadPool():创建一个可根据需要创建新线程的线程池
Executors.newFixedThreadPool(n):创建一个可重用固定线程数的线程池
Executors.newSingleThreadExecutor():创建一个只有一个线程的线程池
Executors.newScheduledThreadPool(n):创建一个线程池,它可安排在给定延迟后运行命令或者定期地执行。
class NumThread implements Runnable{
@Override
public void run() {
for(int i = 0 ; i < 100 ; i ++){
if(i % 2 == 0){
System.out.println(i);
}
}
}
}
class NumThread2 implements Callable<Integer>{
@Override
public Integer call() throws Exception {
int num = 0 ;
for(int i = 0 ; i < 100 ; i ++){
if(i % 2 == 0){
num += i;
}
}
return num;
}
}
public class ThreadPoolDemo {
public static void main(String[] args) {
// Executors.newCachedThreadPool();//创建一个可根据需要创建新线程的线程池
ExecutorService executorService = Executors.newFixedThreadPool(10);//创建一个可重用固定线程数的线程池
// Executors.newSingleThreadExecutor();//创建一个只有一个线程的线程池
// Executors.newScheduledThreadPool(1);//创建一个线程池,它可安排在给定延迟后运行命令或者定期地执行。
executorService.execute(new NumThread());//参数只能是runnable---适用于runnable
FutureTask<Integer> futureTask = (FutureTask) executorService.submit(new NumThread2());//适合是用于Callable
try {
Integer integer = futureTask.get();
System.out.println(integer);
} catch (InterruptedException e) {
e.printStackTrace();
} catch (ExecutionException e) {
e.printStackTrace();
}
executorService.shutdown();
}
}
步骤:
1.提供提供线程数量的线程池
2.执行指定的线程的操作,需要指定实现Runnable接口或Callable接口实现类的对象
3.关闭连接池(不用的时候)
线程池的实现类为:java.util.concurrent.ThreadPoolExecutor
如果要实现对线程池的属性修改,即需要转换类型为ThreadPoolExecutor后对其进行修改: