Java之多线程
1 多线程相关的概念
程序:是为了完成某个特定的任务,使用各种语言编写的指令集合是静态的。
进程:是一次程序执行的状态,或者正在运行的程序是动态的。它对应着从代码加载,执行至执行完毕的一个完整的过程,是一个动态的实体,它有自己的生命,系统在运行时会为每个进程分配不同的内存区域。
线程:进程的进一步细化的线程,也是程序内部执行的一条路径。线程只是一个进程的不同执行路径,每个线程又对应着各自独立的生命周期,拥有独立的运行栈和程序计数器(pc),线程切换的开销小 。线程是进程的一个实体,是CPU调度和分派的 。
多线程:指的是这个程序(一个进程)运行时产生了不止一个线程。
一个Java应用程序,至少有三个线程:main()主线程,gc() 垃圾回收线程,异常处理线程。
并行:多个cpu实例或多台机器同时执行一段处理逻辑是真正的同时,可以理解为指在同一时刻,有多条指令在多个处理器上同时执行
并发:通过cpu调度算法,让用户看上去同时执行,实际上从cpu操作层面不是真正的同时。指在同一时刻只能有一条指令执行,但多个进程指令被快速轮换执行,使得在宏观上具有多个进程同时执行的效果。
线程安全:指在并发的情况之下,该代码经过多线程使用,线程的调度顺序不影响任何结果。这个时候使用多线程,我们只需要关注系统的内存,cpu是不是够用即可。反过来,线程不安全就意味着线程的调度顺序会影响最终结果。
同步:Java中的同步指的是通过人为的控制和调度,保证共享资源的多线程访问成为线程安全,来保证结果的准确。在保证结果准确的同时,提高性能,才是优秀的程序,线程安全的优先级高于性能。
线程同步与线程安全关系:线程安不安全,取决于线程间操作的对象是否有共同变量,如果每个线程对应一个对象相互不影响不用考虑线程同步;如果线程操作的对象有共同变量,要保证线程安全就需要保证线程同步。
Java中的线程分为两类:一种是守护线程,一种是用户线程 ,它们在几乎每个方面都是相同的,唯一的区别是判断JVM何时离开 。
用户线程: 是用户创建的一般线程,如继承Thread类或实现Runnable接口等实现的线程。
守护线程:是为用户线程提供服务的线程,如JVM的垃圾回收、内存管理等线程。在start()方法前调用thread.setDaemon(true)可以把一个用户线程变成一个守护线程。
守护线程和用户线程的区别:当一个用户线程结束后,JVM会检查系统中是否还存在其他用户线程,如果存在则按照正常的调用方法调用。但是如果只剩守护线程而没有用户线程的话,JVM就会终止。
多线程的优点 :①提高应用程序的响应,增强用户体验; ②提高计算机系统CPU的利用率 ;③改善程序结构,将既长又复杂的进程分为多个线程,独立运行,利于理解和修改
何时使用多线程:①程序需要同时执行两个或多个任务;②程序需要实现一些需要等待的任务时,如用户输入、文件读写操作、网络操作、搜索等。③需要一些后台运行的程序。
2 多线程的实现方式
JDK1.5之前创建新执行线程有两种方法:①继承Thread类的方式 ;②实现Runnable接口的方式。JDK1.5之后新增了:③实现Callable接口,④线程池的方式。
2.1 继承Thread类
Thread类:每个线程都是通过某个特定Thread对象的run()方法(可称为线程体)来完成操作的,通过该Thread对象的start()方法来启动这个线程,不是直接调用run()
是通过继承Thread类,重写run方法
public class TestExtendsThread {
public static void main(String[] args) {
new MyThread("线程1").start();
new MyThread("线程2").start();
}
}
class MyThread extends Thread {
//常用构造器如下
//Thread():创建新的Thread对象
//Thread(String threadname):创建线程并指定线程实例名
//Thread(Runnable target):指定创建线程的目标对象,它实现了Runnable接口中的run方法
//Thread(Runnable target, String name):创建新的Thread对象
public MyThread(String name) {
super(name);
}
@Override
public void run() {
for(int i = 0 ;i <50;i++) {
System.out.println(Thread.currentThread().getName() + ":" + i);
}
}
}
继承Thread实现多线程步骤:①定义一个类继承Thread类 ②重写Thread类中的run方法 ③创建这个类的对象(Thread子类),即创建了线程对象 ④调用线程对象start方法(会启动线程,调用run方法 )。
注意:①如果手动调用run()方法,那么就只是普通方法,没有启动多线程模式,想要启动多线程,必须调用start方法 ②run()方法由JVM调用,什么时候调用,执行的过程控制都有操作系统的CPU调度决定③一个线程对象只能调用一次start()方法启动,否则将抛出异常“IllegalThreadStateException”。
2.2 实现Runnable接口
是通过实现Runnable接口,重写run方法,实现Runnable接口的实现类的实例对象作为Thread构造函数的参数,通过调用start()方法启动线程
public class TestExtendsThread {
public static void main(String[] args) {
MyThread mt = new MyThread();
new Thread(mt,"线程1").start();
new Thread(mt,"线程2").start();
}
}
class MyThread implements Runnable {
@Override
public void run() {
for(int i = 0 ;i <50;i++) {
System.out.println(Thread.currentThread().getName() + ":" + i);
}
}
}
通过Runnable实现多线程步骤:①定义一个类实现Runnable接口 ②重写Runnable接口中的run方法 ③通过Thread类含参构造器创建线程对象 ④将Runnable接口的子类对象作为实际参数传递给Thread类的构造器中 ⑤调用Thread类的start方法:开启线程,调用Runnable子类接口的run方法。
继承方式和实现方式的区别:实现方式避免了单继承的局限性,多个线程可以共享同一个接口实现类的对象,非常适合多个相同线程来处理同一份资源。
2.3 通过Callable和FutureTask创建线程
与使用Runnable相比, Callable功能更强大些 ,相比run()方法,可以有返回值,方法可以抛出异常,支持泛型的返回值,需要借助FutureTask类 。
Future接口,可以对具体Runnable、Callable任务的执行结果进行取消、查询是否完成、获取结果等。FutrueTask是Futrue接口的唯一的实现类。FutureTask 同时实现了Runnable, Future接口。它既可以作为Runnable被线程执行,又可以作为Future得到Callable的返回值 。
创建Callable接口的实现类 ,并实现Call方法;创建Callable实现类的实现,使用FutureTask类包装Callable对象,该FutureTask对象封装了Callable对象的Call方法的返回值;使用FutureTask对象作为Thread对象的参数创建并启动线程;调用FutureTask对象的get()来获取子线程执行结束的返回值。
public class TestExtendsThread {
public static void main(String[] args) throws InterruptedException, ExecutionException {
Callable<Object> callable = new MyThread();
//启动一个线程
FutureTask<Object> fTask = new FutureTask<Object>(callable);
Thread t1 =new Thread(fTask);
t1.start();
//获取返回值
System.out.println(fTask.get());
//启动另一个线程
FutureTask<Object> fTask1 = new FutureTask<Object>(callable);
Thread t2 =new Thread(fTask1);
t2.start();
//获取返回值
System.out.println(fTask1.get());
}
}
class MyThread implements Callable<Object> {
@Override
public Object call() throws Exception {
for(int i = 0 ;i <50;i++) {
System.out.println(Thread.currentThread().getName() + ":" + i);
}
return null;
}
}
2.4 通过线程池创建线程
经常创建和销毁、使用量特别大的资源,比如并发情况下的线程,对性能影响很大。可以通过提前创建好多个线程,放入线程池中,使用时直接获取,使用完放回池中。可以避免频繁创建销毁、实现重复利用。通过重用现有的线程池,而不是创建新的线程,可以在处理多个请求的时候分摊在线程创建和销毁过程中产生的巨大开销,当请求到达的时候工作线程已经存在,不会由于等待创建线程而延迟执行,从而提高系统的响应性。
线程池好处:①提高响应速度(减少了创建新线程的时间),②降低资源消耗(重复利用线程池中线程,不需要每次都创建);③便于线程管理
(1)ThreadPoolExecutor 用于创建线程池的类,其构造器参数如下:
//corePoolSize:线程池核心线程个数,默认线程池线程个数为 0,只有接到任务才新建线程
//maximumPoolSize:线程池最大线程数量
//keepAliveTime:线程池空闲时,线程存活的时间,当线程池中的线程数大于 corePoolSize 时才会起作用
//unit:时间单位
//workQueue:阻塞队列,当达到线程数达到 corePoolSize 时,将任务放入队列等待线程处理
//threadFactory:线程工厂
//handler:线程拒绝策略,当队列满了并且线程个数达到 maximumPoolSize 后采取的策略
ThreadPoolExecutor(int corePoolSize, int maximumPoolSize, long keepAliveTime, TimeUnit unit, BlockingQueue<Runnable> workQueue, ThreadFactory threadFactory, RejectedExecutionHandler handler)
(2)队列阻塞有四种如下:
ArrayBlockingQueue | 基于数组、有界,按 FIFO(先进先出)原则对元素进行排序 |
LinkedBlockingQueue | 基于链表,按FIFO (先进先出) 排序元素,吞吐量通常要高于 ArrayBlockingQueue |
SynchronousQueue | 每个插入操作必须等到另一个线程调用移除操作,否则插入操作一直处于阻塞状态,吞吐量通常要高于 LinkedBlockingQueue |
PriorityBlockingQueue | 具有优先级的、无限阻塞队列 |
(3)拒绝策略有四种如下:
CallerRunsPolicy | 如果发现线程池还在运行,就直接运行这个线程 |
DiscardOldestPolicy | 在线程池的等待队列中,将头取出一个抛弃,然后将当前线程放进去 |
DiscardPolicy | 默默丢弃,不抛出异常 |
AbortPolicy | java默认,抛出一个异常(RejectedExecutionException) |
(4)实现原则:
当前池大小 poolSize < corePoolSize | 创建新线程执行任务 |
当前池大小 poolSize > corePoolSize,等待队列未满 | 进入等待队列 |
当前池大小 poolSize > corePoolSize,且小于 maximumPoolSize ,且等待队列已满 | 创建新线程执行任务 |
当前池大小 poolSize > corePoolSize,且大于 maximumPoolSize ,且等待队列已满 | 调用拒绝策略来处理该任务 |
注意:线程池里的每个线程执行完任务后不会立刻退出,而是会去检查下等待队列里是否还有线程任务需要执行,如果在 keepAliveTime 里等不到新的任务了,那么线程就会退出。
(5)内置线程池, java.util.concurrent 包中提供了内置线程池,底层都是通过ThreadPoolExecutor获取
Executors.newSingleThreadExecutor() | 单条线程 | 主要用于串行(顺序执行)操作场景 |
Executors.newFixedThreadPool(int n) | 固定数目线程的线程池 | 主要用于负载比较重的场景,为了资源的合理利用,需要限制当前线程数量 |
Executors.newCachedThreadPool() | 创建一个可缓存的线程池,调用execute 将重用以前构造的线程(如果线程可用)。如果现有线程没有可用的,则创建一个新线程并添加到池中。终止并从缓存中移除那些已有 60 秒钟未被使用的线程。 | 主要用于并发执行大量短期的小任务,或者是负载较轻的服务器 |
Executors.newScheduledThreadPool(int n) | 支持定时及周期性的任务执行的线程池,多数情况下可用来替代 Timer 类。 | ScheduledExecutorService 继承 ThreadPoolExecutor |
(6)提交任务:获取 ExecutorService 对象后通过两种方式提交:①void execute():提交不需要返回值的任务②Future<T> submit(): 提交需要返回值的任务
代码例子如下:
public class TestExtendsThread {
//线程池数量
private static int POOLNUM = 5;
public static void main(String[] args) throws InterruptedException, ExecutionException {
//创建线程池
ExecutorService threadPool = Executors.newFixedThreadPool(3);
for(int i = 0;i<POOLNUM;i++) {
MyThread mt = new MyThread();
threadPool.execute(mt);
}
//关闭线程池
threadPool.shutdown();
}
}
class MyThread implements Runnable {
@Override
public void run() {
for(int i = 0 ;i <10;i++) {
System.out.println(Thread.currentThread().getName() + ":" + i);
}
}
}
总结:(1)继承Thread和实现Runnable接口可以归结为一类:无返回值,原因很简单,通过重写run方法,run方式的返回值是void,所以没有办法返回结果。(2)实现Callable节课和线程池可以归结成一类:有返回值,通过Callable接口,就要实现call方法,这个方法的返回值是Object,所以返回的结果可以放在Object对象中。
3 线程中的常用方法
方法 | 描述 |
void start() | 此线程开始执行; Java虚拟机调用此线程的run方法 |
void run() | 多线程启动后执行的内容 |
static Thread currentThread | 返回当前的线程对象引用 |
void setName(String name) | 给当前线程对象赋名 |
static void yield() | 当前线程释放CPU执行权,暂停当前正在执行的线程,把执行机会让给优先级相同或更高的线程,若队列中没有同优先级的线程,忽略此方法 |
void join() | 在一个线程A中调用了B线程join(),A线程停止执行被阻塞 ,直到B线程执行完,A才接着执行 |
void setDaemon(boolean on) | 在启动(start())设置当前线程为守护线程 |
boolean isAlive( | 判断线程是否存活 |
wait() | 使当前线程等待状态 |
otify() | 唤醒wait()一个线程 |
notifyAll() | 唤醒wait() 多个线程 |
sleep(long l ) | 当前线程休眠(毫秒),在指定时间段内放弃对CPU控制,使其他线程有机会被执行,时间到后重排队 |
4 线程的优先级
线程的优先级:(1~10)MAX_PRIORITY 最大的优先级10, MIN _PRIORITY 最小优先级1 ,NORM_PRIORITY 默认5
setPriority(newPriority); //设置当前线程的优先级
getPriority() //获取当前线程的优先级
注意:①线程创建时继承父线程的优先级②低优先级只是获得调度的概率低,并不一定是在高优先级线程之后才被调用
5 线程的生命周期
需要实现多线程,就要在主线程中创建新的线程对象,线程在它的一个完整的生命周期中通常要经历如下的五种状态:
(1)新建:用new关键字和Thread类或其子类建立一个线程对象后就属于新建状态;
(2)就绪:新生状态的线程有自己的内存空间,通过调用start方法进入就绪状态(runnable),还没有分配到CPU,处于线程就绪队列(可运行池,不是按队列顺序取出的);
(3)运行:就绪状态的线程一旦获得CPU,线程就进入运行状态并自动调用自己的run方法,它可以变为阻塞状态、就绪状态和死亡状态,如何转变见上图;
(4)阻塞:处于运行状态的线程在某些情况下,如执行了sleep(睡眠)方法,进入阻塞状态。 在阻塞状态的线程不能进入就绪队列。只有当引起阻塞的原因消除时,线程便转入就绪状态,重新到就绪队列中排队等待,被系统选中后从原来停止的位置开始继续运行。
(5)死亡:当线程的run()方法执行完,或者被强制性地终止,就认为它死去。这个线程对象也许是活的,但是,它已经不是一个单独执行的线程。
几个重要的方法::
sleep(timeout) | 当前线程进入阻塞状态,暂停执行一定时间,不会释放锁标记 |
join() | 使当前线程等待调用 join()方法的线程结束后才能继续执行 |
yield() | 调用该方法的线程重回可执行状态,不会释放锁标记,可以理解为交出 CPU 时间片,但是不一定有效果,因为有可能又被马上执行。该方法的真正作用是使具有相同或者更高优先级的方法得到执行机会。 |
wait(timeout) | 通常和 notify()/notifyAll()搭配使用,当前线程暂停执行,会释放锁标记。进入对象等待池。直到调用 notify()方法之后,线程被移动到锁标记等待池。只有锁标记等待池的线程才能获得锁 |
6 线程的同步
java在多线程并发控制的时候,当多个线程同时操作一个可共享的资源变量时(如数据的增删改查),将会导致数据不准确,相互之间产生冲突,因此加入同步锁以避免在该线程没有完成操作之前,被其他线程的调用,从而保证了该变量的唯一性和准确性。
public class TicketDemo {
public static void main(String[] args) {
Ticket ticket = new Ticket();
new Thread(ticket,"窗口1").start();
new Thread(ticket,"窗口2").start();
new Thread(ticket,"窗口3").start();
}
}
class Ticket implements Runnable{
private int ticks = 50;
public void run() {
while (true){
if(ticks>0){
try {
Thread.sleep(1000);
} catch (InterruptedException e) {
e.printStackTrace();
}
System.out.println(Thread.currentThread().getName()+"售出票号为:"+ticks);
ticks--;
}else {
break;
}
}
}
}
//部分输出结果为
窗口1售出票号为:5
窗口3售出票号为:5
窗口2售出票号为:4
窗口1售出票号为:2
窗口2售出票号为:2
窗口3售出票号为:1
窗口1售出票号为:-1
上面的例子演示了多线程出现了安全问题。出现问题的原因是:当多条语句在操作同一个线程共享数据时,一个线程对多条语句只执行了一部分,还没有执行完,另一个线程参与进来执行,导致共享数据的错误。
相应的解决办法就是:对多条操作共享数据的语句,只能让一个线程都执行完,在执行过程中,其他线程不可以参与执行。
Java对于多线程的安全问题提供了专业的解决方式:同步机制
(1)同步代码块:用synchronized关键字修饰的语句块。该语句块会自动被加上内置锁,从而实现同步
语法:synchronized(监视器){ //锁
//多个线程共享的资源
}
监视器:可以是一个任意的共享对象(多个线程共用的一个对象)
注意:有关继承Thread类 和实现Runnable接口, 实现接口会好一些 :①创建共有的对象接口只有一个, 同步锁的对象可能是this;②类是单继承,不能继承其它类
public class TestSync {
public static void main(String[] args) {
MyThread1 t1 = new MyThread1();
new Thread(t1,"线程1").start();
new Thread(t1,"线程2").start();
}
}
class MyThread1 implements Runnable {
int i = 0;
@Override
public void run() {
while(true) {
synchronized (this) {
if(i>10) {
break;
}
System.out.println(Thread.currentThread().getName() + ":" + i++);
}
}
}
}
(2)同步方法:用synchronized关键字修饰的方法,java的每个对象都有一个内置锁,当用此关键字修饰方法时,内置锁会保护整个方法。在调用该方法前,需要获得内置锁,否则就处于阻塞状态。
语法:权限修饰符 synchronized 返回值 方法名(){}
建议:锁小不锁大
注意: synchronized关键字也可以修饰静态方法,此时如果调用该静态方法,将会锁住整个类;非静态方法锁对象可以用this;注意继承方式实现线程要用静态对象;静态方法锁对象是该类的类型(如:类名.class)
public class TestSync {
public static void main(String[] args) {
new MyThread1("线程1").start();
new MyThread1("线程2").start();
new MyThread1("线程3").start();
new MyThread1("线程4").start();
}
}
class MyThread1 extends Thread {
public MyThread1(String name) {
super(name);
}
static int i = 0;
@Override
public void run() {
while(true) {
if(i>30) {
break;
}
circulation();
}
}
private synchronized void circulation() {
System.out.println(Thread.currentThread().getName() + ":" + i++);
}
}
(3)使用特殊域变量volatile:①volatile为域变量提供了一种免锁机制;②使用volatile修饰域,虚拟机就能知道该域可能会被其他线程更新;③每次使用该域都会重新计算而不是从寄存器中取值;④volatile不会提供任何原子操作,因此不能修饰final修饰的变量。⑤volatile并不能保证线程安全,只能保证线程同步。
注意:多线程中的非同步问题主要出现在对域的读写上,如果让域自身避免这个问题,则就不需要修改操作该域的方法。用final域,有锁保护的域和volatile域可以避免非同步的问题。
public class TestSync {
public static void main(String[] args) {
new MyThread1("线程1").start();
new MyThread1("线程2").start();
new MyThread1("线程3").start();
new MyThread1("线程4").start();
}
}
class MyThread1 extends Thread {
public MyThread1(String name) {
super(name);
}
private static volatile int i = 0;
@Override
public void run() {
while(true) {
if(i>30) {
break;
}
System.out.println(Thread.currentThread().getName() + ":" + i++);
}
}
}
//最后可能会出现33,32等,所以多线程共享同一个变量volatile会保证线程同步,但不一定能保证线程安全,
上面代码运行的结果最后可能会出现33,32等,所以多线程共享同一个变量volatile会保证线程同步,但不一定能保证线程安全,
(4)使用同步锁实现线程同步:jdk1.5后增加了java.util.concurrent包来支持同步,ReentrantLock类是可重入、互斥、实现了Lock接口的锁,它与使用synchronized方法和快具有相同的基本行为和语义。
常用方法:ReentrantLock() : 创建一个ReentrantLock实例,lock() : 获得锁,unlock() : 释放锁。
ReentrantLock()还有一个可以创建公平锁的构造方法,但由于能大幅度降低程序运行效率,不推荐使用。
public class TestSync {
public static void main(String[] args) {
new MyThread1("线程1").start();
new MyThread1("线程2").start();
new MyThread1("线程3").start();
new MyThread1("线程4").start();
}
}
class MyThread1 extends Thread {
public MyThread1(String name) {
super(name);
}
//声明锁
private static Lock lock = new ReentrantLock();
private static volatile int i = 0;
@Override
public void run() {
while(true) {
//获得锁
lock.lock();
if(i>30) {
break;
}
try {
sleep(100);
} catch (InterruptedException e) {
// TODO Auto-generated catch block
e.printStackTrace();
}
System.out.println(Thread.currentThread().getName() + ":" + i++);
//释放锁
lock.unlock();
}
}
}
(5)使用阻塞队列实现线程同步:前面所述的都是在技术底层实现同步的(包括后面的ThreadLocal),我们应该原理底层结构。LinkedBlockingQueue<E>是基于已连接节点的,访问任意的BlockingQueue。常用方法如下
方法 | 描述 |
构造方法LinkedBlockingQueue() | 创建一个容量为Integer.MAX_VALUE的LinkedBlockingQueue |
构造方法LinkedBlockingQueue(int capacity) | 创建一个容量为capacity的LinkedBlockingQueue |
void put(E e) | 在队尾添加一个元素,如果队列满则阻塞 |
E take() | 移除并返回队列头元素 |
int size() | 返回队列中元素个数 |
当队列满时:add()方法会抛出异常,offer()方法返回false,put()方法会阻塞
public class TestLinkedBlockingQueue {
public static void main(String[] args) {
warehouse w = new warehouse();
producer p1 = new producer(w);
consumer p2 = new consumer(w);
new Thread(p1,"生产者线程1").start();
//new Thread(p1,"生产者线程2").start();
new Thread(p2,"消费者线程1").start();
//new Thread(p2,"消费者线程2").start();
}
}
/**
* @Description 生产者
* @author refuel
* @version v1.0
*/
class producer implements Runnable {
warehouse w;
public producer(warehouse w) {
this.w = w;
}
@Override
public void run() {
while(true) {
w.product();
}
}
}
/**
* @Description 消费者
* @author refuel
* @version v1.0
*/
class consumer implements Runnable {
warehouse w;
public consumer(warehouse w) {
this.w = w;
}
@Override
public void run() {
while(true) {
w.consume();
}
}
}
/**
* @Description 仓库
* @author refuel
* @version v1.0
*/
class warehouse {
//定义一个阻塞队列来存储生产出来的产品
LinkedBlockingQueue<Integer> link = new LinkedBlockingQueue<>();
//生产产品
public void product() {
if(link.size() < 30) {
try {
link.put(1);
} catch (InterruptedException e) {
e.printStackTrace();
}
}
System.out.println(Thread.currentThread().getName() + "生产者BlockingQueue====" + link.size());
try {
Thread.currentThread().sleep(500);
} catch (InterruptedException e) {
e.printStackTrace();
}
}
//消费商品
public void consume() {
if(link.size() > 0) {
try {
link.take();
} catch (InterruptedException e) {
e.printStackTrace();
}
}
System.out.println(Thread.currentThread().getName() +"BlockingQueue====" + link.size());
try {
Thread.currentThread().sleep(500);
} catch (InterruptedException e) {
e.printStackTrace();
}
}
}
(6)使用原子变量实现线程同步:原子操作就是指将读取变量值、修改变量值、保存变量值看成一个整体来操作,同时完成,要么都不完成。util.concurrent.atomic包中提供了创建了原子类型变量的工具类,其中AtomicInteger 表可以用原子方式更新int的值,可用在应用程序中(如以原子方式增加的计数器),但不能用于替换Integer;可扩展Number,允许那些处理机遇数字类的工具和实用工具进行统一访问。
AtomicInteger类常用方法:
AtomicInteger(int initialValue) : 创建具有给定初始值的新的AtomicInteger
addAddGet(int dalta) : 以原子方式将给定值与当前值相加
get() : 获取当前值
对于引用变量和大多数原始变量(long和double除外)的读写操作;对于所有使用volatile修饰的变量(包括long和double)的读写操作。
class account{
//使用ThreadLocal类管理共享变量banlance
private static AtomicInteger banlance = new AtomicInteger(50);
public void saveMoney(int money){
banlance.addAndGet(money);
}
public AtomicInteger getBalance(){
return banlance;
}
}
(7)使用局部变量实现线程同步:如果使用ThreadLocal管理变量,则每一个使用该变量的的线程都会获得该变量的副本,副本之间相互独立,这样每个线程都可以随意修改自己的变量副本,不会对其他线程产生影响。
ThreadLocal 类的常用方法:方法
方法 | 描述 |
ThreadLocal() 唯一的构造器 | 创建一个线程本地变量 |
T get() | 返回此线程局部变量的当前线程副本中的值 |
protected T initialValue() | 返回此线程局部变量的当前线程的"初始值" |
void set(T value) | 将此线程局部变量的当前线程副本中的值设置为value |
注意:①TreadLocal不是为了解决并发同步用的,是为了隔离变量用的。同步是为了让多个线程共同操作一个对象而不乱掉,而ThreadLocal直接就把某个对象在各自线程中重新实例化一个了,各个线程都有自己的该对象,所以就不用管同步不同步了。②ThreadLocal与同步机制:a.ThreadLocal与同步机制都是为了解决多线程中相同变量的访问冲突问题; b.前者采用以"空间换时间"的方法,后者采用以"时间换空间"的方式;③在并发编程的时候,各个线程都 在操作同一个变量,成员变量如果不做任何处理其实是线程不安全的,并且我们也知道volatile这个关键字也是不能保证线程安全的。当需要满足这样一个条件: 变量是同一个,但是每个线程都使用同一个初始值,也就是使用同一个变量的一个新的副本。这种情况之下ThreadLocal就非常使用,比如说DAO的数 据库连接,我们知道DAO是单例的,那么他的属性Connection就不是一个线程安全的变量。而我们每个线程都需要使用他,并且各自使用各自的。这种情况,ThreadLocal就比较好的解决了这个问题。
class account{
//使用ThreadLocal类管理共享变量banlance
private static ThreadLocal<Integer> banlance = new ThreadLocal<Integer>(){
@Override
protected Integer initialValue(){
return 100;
}
};
public void saveMoney(int money){
banlance.set(banlance.get()+money);
}
public int getBalance(){
return banlance.get();
}
}
7 同步机制中的锁
(1)同步锁机制中的锁(锁机制): 对于并发工作,要防止共享资源竞争的方法就是当资源被一个任务使用时,在其上加锁。第一个访问某项资源的任务必须锁定这项资源,使其他任务在其被解锁之前,就无法访问它了,而在其被解锁之时,另一个任务就可以锁定并使用它了。
synchronized中的锁是任意对象都可以作为同步锁。所有对象都自动含有单一的锁(监视器)。同步方法的锁:静态方法(类名.class)、非静态方法(this)。同步代码块:自己指定,很多时候也是指定为this或类名.class 。注意: 必须确保使用同一个资源的多个线程共用一把锁,这个非常重要,否则就无法保证共享资源的安全 。一个线程类中的所有静态方法共用同一把锁(类名.class),所有非静态方法共用同一把锁(this),同步代码块(指定需谨慎)。
(2)同步的范围如何确定:
①首先确定代码是否存在线程安全:明确哪些代码是多线程运行的代码;明确多个线程是否有共享数据;明确多线程运行代码中是否有多条语句操作共享数据
②解决:对多条操作共享数据的语句,只能让一个线程都执行完,在执行过程中,其他线程不可以参与执行。即所有操作共享数据的这些语句都要放在同步范围中
③注意:如果范围太小:没锁住所有有安全问题的代码;如果范围太大:没发挥多线程的功能
(3)释放锁的操作
①当前线程的同步方法、同步代码块执行结束
②当前线程在同步代码块、同步方法中遇到break、return终止了该代码块、该方法的继续执行
③当前线程在同步代码块、同步方法中出现了未处理的Error或Exception,导致异常结束
④当前线程在同步代码块、同步方法中执行了线程对象的wait()方法,当前线程暂停,并释放锁
(4)不会释放锁的操作
①线程执行同步代码块或同步方法时
②程序调用Thread.sleep()
③Thread.yield()方法暂停当前线程的执行
④线程执行同步代码块时,其他线程调用了该线程的suspend()方法将该线程挂起,该线程不会释放锁(同步监视器)
⑤应尽量避免使用suspend()和resume()来控制线程
(5)线程的死锁问题
死锁:不同的线程分别占用对方需要的同步资源不放弃,都在等待对方放弃 自己需要的同步资源,就形成了线程的死锁。
死锁现象:出现死锁后,不会出现异常,不会出现提示,只是所有的线程都处于阻塞状态,无法继续
解决方法:专门的算法、原则;尽量减少同步资源的定义;尽量避免嵌套同步
(6)synchronized和Lock对比
synchronized | Lock | |
解释 | Java关键字 | Java接口 |
显示隐式 | 隐式锁,出了作用域自动释放 | 需显示指定起始位置和终止位置(手动开启和关闭锁 ) |
释放锁 | 获取锁的线程会在执行完同步代码后自动释放锁(发生异常时释放锁) | 在finally中必须释放锁,不然容易造成线程死锁 |
等待 | 一个线程获得锁后阻塞,其他线程会一直等待 | 线程不会一直等待,超时会释放 |
锁类型 | 可重入但不可中断、非公平 | 可重入、可中断、可公平也可不公平 |
Lock只有代码块锁,synchronized有代码块锁和方法锁;使用Lock锁,JVM将花费较少的时间来调度线程,性能更好。并且具有更好的扩展性(提供更多的子类)。优先使用顺序:Lock-> 同步代码块(已经进入了方法体,分配了相应资源)-> 同步方法(在方法体之外)
(7)synchronized和volatile
synchronized | volatile | |
是否阻塞 | 会发生线程阻塞 | 不会发生阻塞 |
能修饰部分 | 方法、代码块、类 | 变量 |
能否保证原子性 | 能保证 | 不能保证 |
安全性 | 线程安全 | 非线程安全 |
能够处理的问题 | 多线程之间访问资源的同步性 | 变量在多线程之间的可见性 |
8 线程的通讯
线程之间需要一些协调通信,来共同完成一件任务。Object类中相关的方法有两个notify方法和三个wait方法,他们都是final修饰的不能被重写。
(1)wait()当前线程挂起(处于等待状态),并释放CPU资源(释放锁),同步资源并等待, 别的线程可访问并修改共享资源,而当前线程排队等候其他线程调用notify()或notifyAll()方法唤醒,唤醒后等待重新获得对监视器的所有权后才能继续执行。
(2)notify() 唤醒正在排队等待(wait())的线程中优先级最高者,被唤醒的线程是不能被执行的,需要等到当前线程放弃这个对象的锁,和其他线程以通常的方式进行竞争,来获得对象的锁;
(3)notifyAll() 唤醒正在排队等待(wait())的所有线程。
注意:wait()和notify()方法要求在调用时线程已经获得了对象的锁,因此对这两个方法的调用需要放在synchronized方法或synchronized块中,否则会报java.lang.IllegalMonitorStateException异常。因为这三个方法必须有锁对象调用,而任意对象都可以作为synchronized的同步锁,因此这三个方法只能在Object类中声明。
(4)sleep和wait对比
sleep | wait | |
解释 | Thread类的一个静态函数 | Object类的方法 |
作用 | 使调用线程睡眠(阻塞)一段时间 | 使当前线程阻塞 |
唤醒 | 到了设置的时间自动醒来 | 调用notify(),则被唤醒 |
是否释放锁 | 不释放锁 | 释放锁 |
(5)yield和notify对比
yield | notify | |
解释 | Thread类的方法 | Object类的方法 |
作用 | 使运行中的线程重新变为就绪状态,让同优先级线程重新竞争 | 唤醒单个线程 |
(6)案例
public class ProAndCon {
public static void main(String[] args) {
Manager manager = new Manager();
Prductor prductor = new Prductor(manager);
Consumer consumer = new Consumer(manager);
new Thread(prductor,"prductor1:").start();
new Thread(prductor,"prductor2:").start();
new Thread(consumer,"consumer1:").start();
new Thread(consumer,"consuemr2:").start();
}
}
class Manager{
private int product = 0;
public synchronized void addPro(){
if (product>100){
try {
wait();
} catch (InterruptedException e) {
e.printStackTrace();
}
}else {
product++;
System.out.println(Thread.currentThread().getName()+"生产了"+product+"产品");
notifyAll();
}
}
public synchronized void getPro(){
if (product<=0){
try {
wait();
} catch (InterruptedException e) {
e.printStackTrace();
}
}else {
System.out.println(Thread.currentThread().getName()+"消费了"+product+"产品");
product--;
notifyAll();
}
}
}
class Prductor implements Runnable{
Manager manager;
public Prductor(Manager manager){
this.manager = manager;
}
public void run() {
System.out.println(Thread.currentThread().getName()+"开始生产");
while (true){
try {
Thread.sleep((int) (Math.random()*1000));
} catch (InterruptedException e) {
e.printStackTrace();
}
manager.addPro();
}
}
}
class Consumer implements Runnable{
Manager manager;
public Consumer(Manager manager){
this.manager= manager;
}
public void run() {
System.out.println(Thread.currentThread().getName()+"开始消费");
while (true){
try {
Thread.sleep((int) (Math.random()*1000));
} catch (InterruptedException e) {
e.printStackTrace();
}
manager.getPro();
}
}
}