**
1. 线程安全问题
**
当多个线程并发执行的时候,如果操作同一个数据(共享数据),就会造成共享数据的不准确和不合理。这种现象就是线程不安全,即产生了线程安全问题。
线程安全问题的解决方案:
使用同步机制来解决线程安全问题,线程同步是指在某一时间内只允许一个线程在执行并访问共享数据,又叫互斥。
线程同步的2种方式:
- 同步代码块 2.同步方法
同步代码块语法:
synchronized (同步监视器){
//代码块
}
同步监视器的使用要点:
- .同步监视器必须是Object类型,不能是基本数据类型
- .任何Object对象都可以作为同步监视器(锁),但是多个线程必须共用同一个锁(对象)
如何使用同步代码块?
同步代码块说白是就是使用一个锁对象把某个代码块包起来,把代码块锁住,在某个时刻内只能有1个线程抢到这个锁,就可以进到代码块中执行,其中的代码一定是对共享数据的操作.
同步方法语法:
A. 非静态的同步方法
public synchronized 返回类型 方法名(){
//对共享资源进行操作的代码
}
B. 静态的同步方法
public static synchronized 返回类型 方法名(){
//对共享资源进行操作的代码
}
推荐使用同步代码块
- 线程通信
wait() :等待,让正在执行的线程释放CPU执行权,进入阻塞状态,其它线程有机会执行。
notify():唤醒,唤醒正在排队等待的线程,一次唤醒一个,而且是任意的。
notifyAll():唤醒正在排队等待的所有 线程。
注意:1).以上三个方法必须是在线程获取到锁时,才能调用,即只能在同步方法或代码块中调用。 如果不在同步方法或代码块中调用以上方法,报非法监视器状态异常:
java.lang.IllegalMonitorStateException
- . 以上3个方法的调用者只能是对象锁,例如:
```java
@Override
public void run() {
while (true) {
synchronized (obj) {
//唤醒
obj.notify();
if (i <= 100) {
System.out.println(Thread.currentThread().getName() + "," + i);
i++;
try {
//等待
obj.wait();
} catch (InterruptedException e) {
e.printStackTrace();
}
} else {
break;
}
}
}
}
2. 解决线程安全问题的方法2-使用Lock
lock和synchronized的区别:synchronized对锁的操作是隐式的,而Lock是显式的,即用户必须手动的去加锁和释放锁.
```java
class TicketRunnable implements Runnable {
// 共享数据,保存火车票的总量
private int ticketNum = 100;
//创建锁对象
Lock lock=new ReentrantLock();
@Override public void run() {
while (true) {
try {
//加锁,相当于synchronized(this)
lock.lock(); // 当票数小于等于0时,窗口停止售票,跳出死循环
if (ticketNum <= 0)
break;
// 当票数大于0时,售票窗口开始售票
//Thread.sleep(10);
// 模拟切换线程的操作
// 输出售票窗口(线程名)卖出的哪一张票
String name = Thread.currentThread().getName();
System.out.println(name + "--卖出第" + ticketNum + "张票");
// 卖票之后,总票数递减
ticketNum--;
} catch (Exception e) {
e.printStackTrace();
}finally {
//解锁
lock.unlock();
}
}
}
}
线程的生命周期会经历以下几个状态:
新建:new创建线程对象时
就绪:调用start()方法时
运行:调用run()方法时
阻塞: 多种原因可导致阻塞
死亡:多种原因
新建、就绪状态
使用new 关键字创建一个线程时,该线程处于新建状态
线程对象调用 start()方法时,该线程处于就绪状态(线程获得CPU,等待执行)
线程的启动是从调用start()方法开始的,而不是run()方法
永远不要调用线程对象的run()方法
如果直接调用 run() 方法,系统会把该线程对象当成普通对象,run()方法也将变成一个普通方法(而不是线程执行体)而立即执行。
如果直接调用了 run() 方法,则该线程不再处于新建状态,不能再次调用 start()方法,否则会报IllegalThreadStateException异常
如果直接调用了 run() 方法,则在run() 方法里不能直接通过this.getName() 方法获得线程名(此时获取的是对象名,因为此时已经没有线程体了,线程对象变成了普通对象),而是通过Thread.currentThread().getName() 获得。
如果希望线程对象调用start() 方法后立即执行run() 方法(线程体),可通过Thread.sleep(1)让主线程睡眠1毫秒,而给其他线程执行机会。
运行、阻塞状态
如果CPU是单核,则在任一时刻都只有一个线程在执行。当线程数据大于核数时,就会出现线程轮换。
所有桌面和服务器系统都采用的是抢占式调度策略,即当前线程在系统允许的执行时间之后,就给其他线程获得执行机会,且优先给优化级高的线程。
有些小型系统如手机会采用协作式调度策略,即只能当前线程主动放弃所占资源(调用sleep()或yield()方法)
线程从阻塞状态解除后只能转变为就绪状态,而就绪状态变为运行状态只能由系统线程调度转换。
线程调用了yield()方法也会重新进入就绪状态
发生以下情况时,线程将进入阻塞状态:
调用sleep()方法时。此时会放弃它所占用的处理器资源。【过了sleep指定时间不再阻塞】
调用一个阻塞式IO方法还没有返回之前,该线程被阻塞。【阻塞IO方法返回后不再阻塞】
试图获取一个正被其他线程所持有的同步监视器。【拿到监听器不再阻塞】
等待通知时(notify)。【其他线程调用了notify时不再阻塞】
调用suspend()方法将程序挂起时。【线程调用resume()方法时撤销挂起时不再阻塞】
线程死亡
线程死亡情况:
线程正常结束(run或call方法执行完)
线程抛出一个未捕获的Exception或Error
线程自己调用stop()方法(该方法容易导致死锁)
一旦子线程启动后,它就拥有和主线程相同的地位,不受主线程的影响。
线程对象只能调用一次start()方法,且只能在新建状态时才能调用。否则会抛出IllegalThreadStateException异常。
线程池
Java中的线程池是运用场景最多的并发框架,几乎所有需要异步或并发任务的程序都可以使用线程池,在开发过程中,合理的使用线程池能带来3个好处。
第一:降低资源消耗,通过重复利用已创建的线程降低线程创建和销毁造成的消耗。
第二:提高响应速度,当任务到达时,任务可以不需要等到线程创建就能立即执行。
第三:提高线程的可管理性。线程是稀缺资源,如果无限制的创建,不仅会消耗系统资源,还会降低系统的稳定性,使用线程池可以进行统一的分配、调优和监控。但是,要做到合理使用线程池,必须对其实现原理了如指掌。
我们可以通过ThreadPoolExecutor来创建一个线程池。
new ThreadPoolExecutor(corePoolSize,maximumPoolSize,keepAliveTime,
milliseconds,runnableTaskQueue,handler);
创建一个线程池时需要输入几个参数,如下:
- corePoolSize(线程基本大小):当提交一个任务到线程池时,线程池会创建一个线程来执行任务,即时其他空闲的基本线程能够执行新任务也会创建线程,等到需要执行的任务数大于线程池基本大小时就不再创建,如果调用线程池的prestartAllCoreThreads()方法,线程池会提前创建并启动所有的基本线程。
- runableTaskQueue(任务队列):用于保存等待执行的任务的阻塞队列,可以选择一下几个阻塞队列:
ArrayBlockingQueue:是一个基于数组结构的有界阻塞队列,此队列按FIFO(先进先出)原则对元素进行排序。
LinkedBlockingQueue:是一个基于链表结构的阻塞队列,此队列按FIFO排序元素,吞吐量通常要高于ArrayBlockingQueue。静态工厂方法Executors.newFixedThreadPool()使用了这个队列。
SynchronousQueue:一个不存储元素的阻塞队列,每个插入操作必须等到另一个线程调用移除操作,否则插入操作一直处于阻塞状态,吞吐量通常要高于LinkedBlockingQueue,静态工厂方法Executors.newCachedThreadPool使用了这个队列。
PriorityBlockingQueue:一个具有优先级的无限阻塞队列。
3.maximumPoolSize(线程池最大数量):线程池允许创建最大线程数。如果队列满了,并且已创建的线程数小于最大线程数,则线程池会创建新的线程执行任务,值得注意的是,如果使用了无界的任务队列这个参数就没什么效果。
4.ThreadFactory:用于设置创建线程的工厂,可以通过线程工厂给每个创建出来的线程设置更有意义的名字,使用开源框架guava提供的ThreadFactoryBuileder()可以快速的给线程池里设置有意义的名字:ThreadFactoryBuilder().setNameFormat(“XX-task-%d”).build()
5.RejectedExecutionHandler(饱和策略):当队列和线程池都满了,说明线程池处于饱和状态,那么必须采取一种策略处理提交的新任务,这个策略默认情况下是AbortPolicy,表示无法处理新任务时抛出异常,在JDK1.5中Java线程池框架提供了以下4种策略。
ZbortPolicy:直接抛出异常。
-CallerRunsPolicy:只用调用者所在线程来运行任务。
-DiscardOldestPolicy:丢弃队列最近的一个任务,并执行当前任务。
-DiscardPolicy:不处理,丢弃掉。
当然,也可以根据应用场景需要来实现RejectedExecutionHandler接口自定义策略。如记录日志或持久化不能处理的任务。
-keepAliveTime(线程活动保持时间):线程池的工作线程空闲后,保持存活的时间。所以,如果任务很多,并且每个任务执行的时间比较短,可以调大时间,提高线程的利用率。
-TimeUnit(线程活动保持时间):可选的单位有天(DAYS)、小时(HOURS)、分钟(MINUTES)、毫秒(MILLISECONDS)、毫秒(MICROSECONDS,千分之一毫秒)和纳秒(NANOSECONDS,千分之一微秒)。
向线程池提交任务
可以使用两个方法向线程池提交任务,分别为execute()和submit()方法,execute()方法用于提交不需要返回值的任务,所以无法判断哪任务是否被线程池执行成功,通过以下代码可知execute()方法输入的任务是一个Runnable类的实例。
threadPool.execute(new Runnable(){
@Overide
public void run(){
//TODO Auto-generated method stub
}
})
submit()方法用于提交需要返回值的任务,线程池会返回一个future类型的对象,通过这个future对象可以判断任务是否执行成功,并且可以通过future的get()方法来获取返回值,get()方法会阻塞当前线程直到任务完成,而使用get(long time out, TimeUnit unit)方法则会阻塞当前线程一段时间后立即返回,这时候有可能任务没有执行完。
Future<Object> future = executor.submit(harReturnValuetask);
try{
Object s = future.get();
}catch(InterruptedException e){
//处理无法执行任务异常
}finally{
//关闭线程池
executor.shutdown();
}
关闭线程池
可以通过调用线程池的shutdown或shutdownNow方法来关闭线程池,它们的原理是遍历线程池中的工作流程,然后逐个调用线程的interrupt方法来中断线程,所以无法响应中断的任务可能永远无法终止,但是它们存在一定的区别,shutdownNow首先将线程池的状态设置成STOP,然后尝试停止所有的正在执行或暂停任务的线程,并返回等待执行任务的列表,而shutdown只是将线程池的状态设置成SHUTDOWN状态,然后中断所有没有正在执行任务的线程。只要调用了这两个关闭方法中的任意一个,isShutdown方法就会返回true,当所有的任务都已关闭后,才表示线程池关闭成功,这时调用isTerminaed方法会返回true,至于应该调用哪一种方法来关闭线程池,应该由提交到线程池的任务特性决定,通常调用shutdown()方法来关闭线程池,如果任务不一定要执行完,则可以调用shutdownNow()方法。
合理配置线程池
要想合理地配置线程池,就必须首先分析任务特性,可以从以下几个角度来分析。
- 任务的性质:CPU密集型任务、IO密集型任务和混合型任务。
- 任务的优先级:高、中和低。
- 任务的执行时间:长、中和短。
- 任务的依赖性:是否依赖其他系统资源,如数据库连接。
性质不同的任务可以用不同规模的线程池分开处理,CPU密集型任务应配置尽可能小的线程,如配置Ncpu+1个线程的线程池。由于IO密集型任务线程并不是一直在执行任务,则应配置尽可能多的线程,如2*Ncpu。混合型的任务,如果可以拆分,将其拆分成一个CPU密集型任务和一个IO密集型任务,只要这两个任务执行的时间相差不是很大,那么分解后执行的吞吐量将高于串行执行的吞吐量,如果这两个任务执行时间相差太大,则没必要进行分解。可以通过Runtime.getRuntime().availableProcessors()方法获得当前设备的CPU个数。优先级不同的任务可以使用优先级队列PriorityBlockingQueue来处理,它可以让优先级高的任务先执行。
注意:如果一直有优先级高的任务提交到队列里,那么优先级低的任务可能永远不能执行,执行时间不同的任务可以交给不同规模的线程池来处理,或者可以使用优先级队列,让执行时间短的任务先执行。依赖数据库连接池的任务,因为线程提交SQL后需要等待数据库返回结果,等待的时间越长,则CPU空闲时间就越长,那么线程数应该设置的越大,这样才能更好地利用CPU。
简单SingleThreadExceutor线程池的使用
1、创建线程
public class AutoTask implements Runnable {
@Override
public void run() {
for (int i = 0; i < 10; i++) {
if (i % 2 == 0) {
System.out.println("偶数打印" + i);
} else {
System.out.println("奇数打印" + i);
}
}
}
}
2、创建线程池,将需要使用到线程池的线程放进线程池。
@Slf4j
public class ThreadPool {
public static void addThread(int i) {
AutoTask autoTask = new AutoTask();
ExecutorService executorService = Executors.newSingleThreadExecutor(
new ThreadFactoryBuilder()
.setNameFormat("thread" + i)
.build());
executorService.execute(autoTask);
log.info("thread启动:"+i );
executorService.shutdown();
}
}
3、调用使用到线程池的方法,以及进行线程休眠。
public class ThreadUtil {
public static void testExceutor() {
try {
for (int i = 0; i < 10; i++) {
ThreadPool.addThread(i);
Thread.sleep(1000);
}
} catch (Exception e) {
e.printStackTrace();
}
}
}