java多线程主要面临的问题就是线程安全问题->
线程安全问题是由线程间的通信造成的,多个线程间不通信就没有线程安全问题->
java中线程通信只能通过类变量和实例变量,因此解决线程安全问题就是解决对变量的安全访问问题->
java中解决变量的安全访问采用的是同步的手段,同步是通过锁实现的->
有三种锁能保证变量只有一个线程访问,偏向锁最快但是只能用于从始至终只有一个线程获得锁,轻量级锁较快但是只能用于线程串行获得锁,重量级锁最慢但是可以用于线程并发获得锁,先用最快的偏向锁,每次假设不成立就升级一个重量;另外还有一种与互斥锁不一样的自旋锁【线程自己执行空循环(有限定次数),无需阻塞,cas算法基于此】,自旋锁比较适用于锁使用者保持锁时间比较短的情况
线程启动:
extends thread方式:new TestThread().start()
implements runnable方式:new Thread(new TestThread()).start()
- wait:当线程调用wait()方法时,当前该线程会进入阻塞状态,且释放锁,使用wait方法的时候,必须配合synchronized使用。
- notify:当线程调用notify()方法时,会唤醒一个处于等待该对象锁的线程,不释放锁,使用notify方法的时候,必须配合synchronized使用。
- sleep:当线程调用sleep()方法时,会让出CPU执行权,不释放锁。当指定的时间到了后,会自动恢复运行状态。
- volatile:可见性,它修饰的成员变量在每次被线程访问时,都强迫从内存中重读该成员变量的值;而且,当成员变量发生变化时,强迫线程将变化值回写到共享内存,这样在任何时刻两个不同线程总是看到某一成员变量的同一个值,这就是保证了可见性。(当多个线程操作同一个成员变量的时候,为了提高效率,JVM为每个线程单独复制了一份,这样会导致各个线程读取的数据出现脏数据,所以使用volatile关键字可以解决脏数据问题)。
- synchronized:synchronized为一段操作或内存进行加锁(调用同一对象的所有线程都必须加锁),它具有互斥性。当线程要操作被synchronized修饰的内存或操作时,必须首先获得锁才能进行后续操作;但是在同一时刻只能有一个线程获得相同的一把锁(对象监视器),所以它只允许一个线程进行操作。
- 原子性:类似"a += b"这样的操作不具有原子性,在JVM中"a += b"可能要经过这样三个步骤: (1)取出a和b;(2)计算a+b;(3)将计算结果写入内存。如果有两个线程t1,t2在进行这样的操作。t1在第二步做完之后还没来得及把数据写回内存就被线程调度器中断了,于是t2开始执行,t2执行完毕后t1又把没有完成的第三步做完。这个时候就出现了错误,相当于t2的计算结果被无视掉了。所以上面的买碘片例子在同步add方法之前,实际结果总是小于预期结果的,因为很多操作都被无视掉了。类似的,像"a++"这样的操作也都不具有原子性,需要配合synchronized实现原子性。java的concurrent包下提供了一些原子类,我们可以通过阅读API来了解这些原子类的用法。比如:AtomicInteger、AtomicLong、AtomicReference等。
- 调用start()方法的顺序不代表线程的启动顺序,线程启动的顺序具有不确定性。
- 如果在start后调用join方法,则主线程必须等待子线程执行完毕才继续运行。如果join使用参数(等待时间),则主线程必须等待子线程执行相应的时间后才继续运行。
runnable&callable
- Callable 使用 call() 方法, Runnable 使用 run() 方法
- call() 可以返回值, 而 run()方法不能返回。
- call() 可以抛出受检查的异常,比如ClassNotFoundException, 而run()不能抛出受检查的异常。
线程池的优点
1、线程是稀缺资源,使用线程池可以减少创建和销毁线程的次数,每个工作线程都可以重复使用。
2、可以根据系统的承受能力,调整线程池中工作线程的数量,防止因为消耗过多内存导致服务器崩溃。
线程池的创建
1 public ThreadPoolExecutor(int corePoolSize, int maximumPoolSize, long keepAliveTime,
TimeUnit unit, BlockingQueue<Runnable> workQueue, RejectedExecutionHandler handler)
corePoolSize:线程池核心线程数量
maximumPoolSize:线程池最大线程数量
keepAliverTime:当活跃线程数大于核心线程数时,空闲的多余线程最大存活时间
unit:存活时间的单位
workQueue:存放任务的队列
handler:超出线程范围和队列容量的任务的处理策略
运行原理
当一个任务通过execute(Runnable)方法添加到线程池时
如果此时线程池中的数量小于corePoolSize,创建新的线程来处理被添加的任务。
如果此时线程池中的数量等于 corePoolSize,则新任务被添加到workQueue队列中,直到workQueue队列满,但不超过maximumPoolSize。
如果此时线程池中的数量大于corePoolSize,缓冲队列workQueue满,并且线程池中的数量小于maximumPoolSize,建新的线程来处理被添加的任务。
如果此时线程池中的数量大于corePoolSize,缓冲队列workQueue满,并且线程池中的数量等于maximumPoolSize,那么通过 handler所指定的策略来处理此任务。
处理任务的优先级为:核心线程corePoolSize、任务队列workQueue、最大线程maximumPoolSize,如果三者都满了,使用handler处理被拒绝的任务。
三种阻塞队列【workQueue】
BlockingQueue<Runnable> workQueue = null;
workQueue = new ArrayBlockingQueue<>(5);//基于数组的先进先出队列,有界
workQueue = new LinkedBlockingQueue<>();//基于链表的先进先出队列,无界
workQueue = new SynchronousQueue<>();//无缓冲的等待队列,无界
四种拒绝策略【handler】
RejectedExecutionHandler rejected = null;
rejected = new ThreadPoolExecutor.AbortPolicy();//默认,队列满了丢任务抛出异常
rejected = new ThreadPoolExecutor.DiscardPolicy();//队列满了丢任务不异常
rejected = new ThreadPoolExecutor.DiscardOldestPolicy();//将最早进入队列的任务删,之后再尝试加入队列
rejected = new ThreadPoolExecutor.CallerRunsPolicy();//如果添加到线程池失败,那么主线程会自己去执行该任务
五种线程池
ExecutorService threadPool = null;
threadPool = Executors.newCachedThreadPool();//使用SynchronousQueue创建线程池,线程数 JVM 控制,不对任务进行缓存。新进任务直接提交给线程池,当线程池中没有空闲线程时,创建一个新的线程处理此任务。这种策略需要线程池具有无限增长的可能性。
threadPool = Executors.newFixedThreadPool(3);//使用LinkedBlockingQueue创建线程池, 固定大小的线程池,当所有 corePoolSize 线程都忙时新任务在队列中等待。这样,创建的线程就不会超过 corePoolSize。(因此,maximumPoolSize 的值也就无效了。)适合于每个任务执行互不影响时
threadPool = Executors.newSingleThreadExecutor();//单线程的线程池,只有一个线程在工作,同上
threadPool = Executors.newScheduledThreadPool(2);//使用DelayedWorkQueue创建线程池,支持定时及周期性执行任务
threadPool = new ThreadPoolExecutor();//默认线程池,可控制参数比较多