多线程
在java程序中同时执行多个线程,每个线程独立执行不同的任务.
可以提高程序的性能和资源利用率,增加程序的并发性.
多线程的作用
1,提高程序性能
可以将一个任务分解成多个子任务并行处理,从而提高程序的运行速度
2,提高资源利用率
可以更好地利用CPU资源,提高CPU的利用率
3,提高程序的并发性
可以使程序同时处理多个任务,提高程序的并发处理能力
创建线程的方式
1,继承Thread类
重写run()方法定义线程的执行逻辑,创建该类实例
调用start()方法启动线程
优点:编码简单
缺点:继承Thread类就无法继承其他类不利于功能扩展
run方法中异常只能捕获不能抛出
MyThread mythread = new MyThread();
mythread.start();
//调用start()方法启动线程(启动后执行run()方法)
class MyThread extends Thread{
//重写run方法
}
2,实现Runnable接口
创建一个实现了Runnable接口的类,实现其run()方法,定义线程的执行逻辑.
创建Thread类实例,将之前实现的对象作为参数传递给Thread的构造函数
最后调用Thread实例的start()方法来启动线程
优点:只需实现接口,仍可以继承和实现,扩展性强
缺点:需要多一个Runnable对象
run方法中异常只能捕获不能抛出
MyRunnable myRunnable = new MyRunnable();
//创建接口实现类对象
Thread thread = new Thread(myRunnable);
//创建线程对象
thread.start();
//开启线程
//任务类
class MyRunnable implements Runnable{
//重写run()方法
}
3,实现Callable接口:
创建一个实现了Callable接口的类,重写接口中的call抽象方法,定义任务并交给Thread类对象完成.
优点:扩展性强(同上),可以获取线程执行结果(有返回值)
缺点:编码复杂
异常可以抛出
Mycallable call = new Mycallable();
//创建任务类对象
FutureTask<String> task = new FutureTask<>(callable);
//继承于Runnable和Future
//定义桥梁
Thread thread = new Thread(task);
//创建线程对象
thread.start();
//开启线程
.............
task.get().var;
//具备阻塞效果,会让task对应的线程先执行完再执行下面的代码
//获取线程执行完毕返回的结果值
class MyCallable implements Callable<String>{
//泛型里是call方法的返回值类型
//重写call()方法
}
public interface Future<V> {
boolean cancel(boolean mayInterruptIfRunning);
//isCancelled:如果此任务在正常完成之前被取消,则返回true。
boolean isCancelled();
//isDone:如果此任务完成,则返回true。
boolean isDone();
//get:等待任务完成,然后返回其结果。是一个阻塞方法
V get() throws InterruptedException, ExecutionException;
//get(long timeout, TimeUnit unit):等待任务完成,然后返回其结果。
//如果在指定时间内,还没获取到结果,就直接返回null。
V get(long timeout, TimeUnit unit)
//TimeUnit是一个枚举类,对象为时间类型,这里传入前面timeout的时间类型
throws InterruptedException, ExecutionException, TimeoutException;
}
/*cancel:用来取消任务,如果取消任务成功则返回true,如果取消任务失败则返回false。
*参数mayInterruptIfRunning表示是否取消正在执行却没有执行完毕的任务,如果设置true,
*则表示可以取消正在执行过程中的任务。
*如果任务正在执行,若mayInterruptIfRunning设置为true,则返回true,
*若mayInterruptIfRunning设置为false,不会取消恩物,返回false;如果任务还没有执行则无论*mayInterruptIfRunning为true还是false,肯定返回true。
*如果任务已经完成则无论mayInterruptIfRunning为true还是false,一定会返回false;*/
Thread类
一个表示线程的类
提供了一系列的方法用于线程操作
构造器
public Thread(String name)
//可以为当前线程指定名称
public Thread(Runnable target)
//封装一个Runnable对象为线程对象
public Thread(Runnable target,String name)
//封装Runnable对象成为线程对象,并指定线程名称
常用方法
public void run()
//线程的任务方法
public void start()
//启动线程
public String getName()
//获取当前线程的名称,线程名称默认为Thread-索引
public void setName(String name)
//为线程设置名称
public static Thread currentThread()
//获取当前执行的线程对象
public static void sleep(long time)
//让当前执行的线程休眠多少毫秒后继续执行
public final void join()....
//让调用当前方法的线程先执行完
//具备阻塞作用
线程安全问题
java中的线程安全问题是指多个线程同时访问共享资源时进行操作
如果执行顺序不确定,可能会导致数据不一致或者程序出错的问题
Account account1;
new Thread(new AccountThread(account1),"小明").start()
//线程1
new Thread(new AccountThread(account1),"小红").start()
//线程2
//多个线程对同一个对象修改
class AccountThread implements Runnable{
private Account account;
public AccountThread(Account account){
this.account = account;
}
@Override
public void run(){//线程启东时执行对对象操作
account.drawMoney(100000);
}
}
class Account{}
如上所示,因为对象在堆内存中被共享,所以出现多个线程对同一个对象执行操作时,就会出现对象的线程安全问题.
线程不安全的原因
1、线程是抢占式执行
线程的抢占式执行就是在一个线程的执行过程中,另一个更优先的进程会抢占当前线程执行的任务,当前线程就会被迫中断,这是引发线程不安全的根本原因,但是线程的调度是随机的,这是由系统决定的,我们无法改变。
2、多线程共享同一变量
多线程共享同一变量如果只是读操作就不会引发线程安全问题,但是如果多线程都修改同一变量就会引发安全问题,就是引例当中的情况,因为修改操作不是原子性,需要多步完成就有可能发生线程抢占导致中断。
3、对变量的操作不是原子性
操作原子性也就是操作能一步完成,但是修改变量的操作就可以分为:将变量从内存加载到寄存器(load) 、修改变量(update)、把寄存器的值加载回内存(save)
例如是自增操作: 假设内存中的变量的初始值为0,t1就先把0加载到寄存器,但是t2进行抢占,也从内存中把0加载到寄存器然后自增为1,然后将1加载回内存,然后t1再自增为1,再将1加载回内存,按道理两次自增应该为2,但是由于线程的抢占以及自增操作是非原子的就会出现上述情况。
4、内存可见性
变量通常存放在内存中,线程对变量操作需要首先从内存中拿出到寄存器,但是一个线程频繁进行读操作,就可能会直接从寄存器上读,不再进入内存这就引发线程不安全,因为线程得不到内存中变量的最新值。
5、指令重排序
指令重排序是编译器的优化操作来提高代码运行的效率,但是对于多线程在进行指令重排序时就可能会出现错误引发线程安全问题。
线程同步-同步代码块
多个线程间协调工作,保证线程安全和正确性的技术
在多线程环境下,如果多个线程同时访问共享资源,可能会导致数据的不一致,而线程同步就可以解决线程安全问题.
线程同步和加锁是密不可分的,加锁是线程同步的一种手段,用于实现多个线程对共享资源的访问控制.
死锁:
当线程间都在互相等待对方的资源时,就会出现死锁
注意事项
1,在线程同步中,当一个线程需要访问共享资源时,会先尝试获取锁,如果锁没有被其他线程占用,则获取成功
2,在获取锁后,该线程就可以访问共享资源了
3,其他线程在访问共享资源之前,必须等待该线程释放锁之后才能获取锁并继续访问共享资源.
4,通过加锁,可以保证同一时间只有一个线程能够访问共享资源,从而避免多个线程同时修改共享资源导致的数据不一致或者冲突问题.加锁是实现线程安全的重要手段之一.
同步代码块(锁对象)
在一段代码前后使用synchronized关键字包裹,来实现线程同步的功能.
synchronized(需要上锁的对象){
//代码块
}
对于同时执行的线程来说,锁对象必须为同一个.
使用同步代码块的作用是确保在某个时刻只有一个线程能执行这段代码,从而避免多个线程同时访问共享资源时出现的并发问题.
同步方法
使用synchronized关键字修饰方法,使得在多线程环境下,只有一个线程能执行这个方法.
修饰符 synchronized 返回值类型 方法名称(参数){
操作共享资源的代码
}
同步方法的底层原理
1,底层原理锁住整个方法代码范围的隐式锁对象
如果方法为实例方法:同步方法默认使用this作为锁对象
如果方法为静态方法:同步方法默认使用类名.class作为锁对象
锁对象详解
1,对象锁
锁住对象,不同实例的锁互相不影响
synchronized关键字在普通方法上
synchronized(this)
锁住当前线程传入的对象,其他对象不受影响
2,类锁
synchronized关键字加在静态方法上类锁
synchronized(类.class),等同于(synchronized(object obj))
obj为静态对象
锁定所有对象,因为所以对象共享同一个静态方法
Lock锁
jdk5开始提供的一个新的锁定操作,可以创建锁对象进行加锁和解锁,更灵活也更强大
Lock是一个接口,不能实例化但他的实现类ReentrantLock可以构建Lock锁对象
锁住lock和unlock之间的代码,类似于同步代码块
构造器
public ReentrantLock()
//获得Lock锁的实现类对象
常用方法
void lock()//获得锁
void unlock()//释放锁
线程通信
多个线程之间交互和通信的方式
通过Object类中的wait()和notify()方法实现多个线程之间的等待-唤醒操作,实现线程的同步和通信,可以使用这种方式来实现生产者-消费者模式,等待-通知模式等常见的多线程编程模式
常用方法
void wait()
//当前线程等待,直到另一个线程调用notify()或者notifyAll()唤醒自己
void notify()
//唤醒正在等待对象监视器(锁对象)的单个线程
void notifyAll()
//唤醒正在等待对象监视器(锁对象)的所有线程
wait和notify必须使用同一把锁调用
wait方法会释放锁,但notify方法不会
线程池
一种线程复用技术,可以减少线程的创建和销毁次数,提高程序的性能和响应速度.
维护了一组工作线程,接受任务并执行,任务执行完毕后,工作线程可以再次被复用,从而避免了每次执行任务都创建新线程的开销.
线程池的优势
1,提高程序性能
重复使用线程,减少创建和销毁线程所消耗的时间和资源,提高了程序性能.
2,提供更好的控制
线程池可以控制线程的创建和销毁,可以控制线程的数量,防止过多的线程导致系统资源的浪费.
3,提高响应速度
线程池可以将任务加入队列,等待程序的执行,从而避免阻塞线程,提高响应速度.
线程池的创建和使用
Executors工厂类:提供了一些静态方法创建不同类型的线程池对象(不建议使用)
public static ExecutorService newFixedThreadPool(int nThreads)
//创建固定线程数量(nThreads)的线程池,如果某个线程因为执行异常而结束,那么线程池会补充一个新线程来替代它
public static ExecutorService newSingleThreadExecutor()
//创建只有一个线程的线程池对象,如果该线程出现异常而结束,那么线程池会补充一个新的线程
public static ExecutorService newCachedThreadPool()
//线程数量随着任务增加而增加,如果线程任务执行完毕且空闲了60%会被回收掉
public static ScheduleExecutorService newScheduledThreadPool(int corePoolSize)
//创建线程池,可以实现在给定的延迟后运行任务或者定期执行任务
常用方法
void execute(Runnable command)
//执行任务/命令,没有返回值,一般用来执行Runnable任务
Future<T> submit(Callable<T> task)
//执行任务,返回未来任务对象获取线程结果,一般拿来执行Callable任务
void shutdown()
//等任务执行完毕后关闭线程池
//一般不关闭
List<Runnable> shutdownNow()
//立刻关闭,停止正在执行的任务,并返回队列中未执行的任务
自定义线程池对象(重点)
java提供了一个代表线程池的接口:ExecutorService
可以使用ExecutorService的实现类ThreadPoolExecutor创建一个线程池对象
ThreadPoolExecutor构造器
public ThreadPoolExecutor(
int corePoolSize,
int maximumPoolSize,
long keepAliveTime,
TimeUnit unit,
BlockingQuene<Runnable> workQueue,
ThreadFactory threadFactory,
RejectedExecutionHandler handler
)
corePoolSize:指定线程池的核心线程数量
maximumPoolSize:指定线程池的最大线程数量(核心+临时)
keepAliveTime:指定临时线程的存活时间
unit:指定临时线程存活的时间单位(秒,分,时,天)
workQueue:指定线程池的任务队列,阻塞队列(BlockingQueue<>的实现类对象)
threadFactory:指定线程池的线程工厂(创建线程的地方)
handler:指定线程池的任务拒绝策略(任务队列满时,新任务来怎么处理)
任务拒绝策略
ThreadPoolExecutor.AbortPolicy
//丢弃任务并抛出RejectedExecutionException异常(默认)
ThreadPoolExecutor.DiscardPolicy
//丢弃任务,但不抛出异常(不推荐)
ThreadPoolExecutor.DiscardOldestPolicy
//抛弃队列中等待最久的任务,然后把任务加入队列中
ThreadPoolExecutor.CallerRunsPolicy
//由主线程负责调用任务的run()方法从而绕过线程池执行
//只会执行超出队列的线程
并发和并行
正在运行的程序(软件)就是一个独立的进程.
线程属于进程,一个进程中可以同时运行多个线程
进程中的线程是并发和并行执行的.
并发
进程中的线程由CPU调度,但cpu同时只能处理一部分线程,为了保证全部线程能向前执行,会轮询每个线程进行服务.
虽然我们感觉线程在同时执行,但实际上是cpu快速的在线程之间切换.这就是并发
并行
在同一个时刻上,多个线程被CPU同时调度执行(多核处理)
线程的生命周期
线程状态
Thread类中有State枚举类
其中枚举项即为以下状态
NEW(新建)
//线程刚被启动,但是并未启动
Runnable(可运行)
//线程已经调用了start(),等待CPU调度
Blocked(锁阻塞)
//线程在执行的时候未竞争到锁对象,则进入Blocked状态
Waiting(无限等待)
//一个线程进入Waiting状态,另一个线程调用notify()或者notifyAll()方法才能够唤醒
Timed Waiting(计时等待)
//在时间抵达之前等待,一般由sleep或者wait设置超时参数得到
Terminated(被终止)
//因为run方法正常退出而死亡,或者因为没有捕获的异常终止了run方法而死亡