多线程以及线程池

本文详细介绍了Java中的线程安全问题,包括同步机制的使用,如同步代码块和同步方法,以及线程通信的wait(), notify(), notifyAll()方法。此外,还探讨了线程生命周期的不同状态,并对比了synchronized与Lock的区别。文章进一步讲解了线程池的概念,分析了线程池的参数配置及其带来的性能优势,并给出了使用示例。最后,讨论了合理配置线程池的策略和线程池的关闭方法。
摘要由CSDN通过智能技术生成

**

1. 线程安全问题

**
当多个线程并发执行的时候,如果操作同一个数据(共享数据),就会造成共享数据的不准确和不合理。这种现象就是线程不安全,即产生了线程安全问题。
线程安全问题的解决方案:
使用同步机制来解决线程安全问题,线程同步是指在某一时间内只允许一个线程在执行并访问共享数据,又叫互斥。
线程同步的2种方式:

  1. 同步代码块 2.同步方法
    同步代码块语法:
    synchronized (同步监视器){
    //代码块
    }
    同步监视器的使用要点:
  1. .同步监视器必须是Object类型,不能是基本数据类型
  2. .任何Object对象都可以作为同步监视器(锁),但是多个线程必须共用同一个锁(对象)
    如何使用同步代码块?

同步代码块说白是就是使用一个锁对象把某个代码块包起来,把代码块锁住,在某个时刻内只能有1个线程抢到这个锁,就可以进到代码块中执行,其中的代码一定是对共享数据的操作.
同步方法语法:
A. 非静态的同步方法
public synchronized 返回类型 方法名(){
//对共享资源进行操作的代码
}
B. 静态的同步方法
public static synchronized 返回类型 方法名(){
//对共享资源进行操作的代码

}

推荐使用同步代码块

  1. 线程通信
    wait() :等待,让正在执行的线程释放CPU执行权,进入阻塞状态,其它线程有机会执行。
    notify():唤醒,唤醒正在排队等待的线程,一次唤醒一个,而且是任意的。
    notifyAll():唤醒正在排队等待的所有 线程。
    注意:1).以上三个方法必须是在线程获取到锁时,才能调用,即只能在同步方法或代码块中调用。 如果不在同步方法或代码块中调用以上方法,报非法监视器状态异常:
    java.lang.IllegalMonitorStateException
  1. . 以上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);

创建一个线程池时需要输入几个参数,如下:

  1. corePoolSize(线程基本大小):当提交一个任务到线程池时,线程池会创建一个线程来执行任务,即时其他空闲的基本线程能够执行新任务也会创建线程,等到需要执行的任务数大于线程池基本大小时就不再创建,如果调用线程池的prestartAllCoreThreads()方法,线程池会提前创建并启动所有的基本线程。
  2. 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();
        }
    }
}
评论
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

当前余额3.43前往充值 >
需支付:10.00
成就一亿技术人!
领取后你会自动成为博主和红包主的粉丝 规则
hope_wisdom
发出的红包
实付
使用余额支付
点击重新获取
扫码支付
钱包余额 0

抵扣说明:

1.余额是钱包充值的虚拟货币,按照1:1的比例进行支付金额的抵扣。
2.余额无法直接购买下载,可以购买VIP、付费专栏及课程。

余额充值