目录
一.概念
1.1 线程,进程,程序
- 线程:线程是进程的一部分,描述指令流执行状态。它是进程中的指令执行流的最小单位,是CPU调度的基本单位。
- 进程:指一个具有一定独立功能的程序,在一个数据集合上的一次动态执行过程。
- 程序:是为完成特定任务,用某种语言编写的一组指令的集合,即指一段静态的代码,静态对象。
程序与进程:一个程序可以包含多个进程,一个进程只能对应一个程序;程序是静态的,是有序代码的集合,进程是动态的,是程序的一个资源分配执行单元。
进程与线程:一个进程可以包含多个线程,一个线程只能对应一个进程;是一个轻量级的子进程,是最小的处理单元。
1.2 并发,并行,串行
- 并发:一个CPU,在同一时间间隔可以处理多个任务
- 并行:多个CPU,在同一时间处理多个任务
- 串行:一个CPU,按照顺序执行多个任务
举例:吃饭时来电话,第一种情况,吃完饭再接电话,串行,第二种,接完电话再吃饭,并发,第三种,边吃饭边接电话,并行。
1.3 同步,异步
- 同步:调用请求发送给被调用者,需要等被调用者返回结果之后才能继续做其它事情。
- 异步:当调用请求发出给被调用者后,调用者不用等待到返回的结果就可以直接继续其它事情。
举例:对讲机,同步;电话,异步。
二.线程
2.1生命周期
- 创建
当一个Thread类或其子类的对象被声明并创建时,新生的线程对象处于新建状态 - 就绪
处于新建状态的线程被start后,将进入线程队列等待CPU时间片,此时它已具备了运行的条件,只是没分配到CPU资源 - 运行
当就绪的线程被调度并获得CPU资源时,便进入运行状态,run方法定义了线程的操作和功能 - 阻塞
在某种特殊情况下,被人为挂起或执行输入输出操作时,让出CPU并临时终止自己的执行,进入阻塞状态
阻塞分类
1.等待阻塞
运行(running)的线程执行wait()方法,JVM会把该线程放入等待队列(waitting queue)中,通过调用notify()方法回到就绪状态
2.同步阻塞
运行(running)的线程在获取对象的同步锁时,若该同步锁被别的线程占用,则JVM会把该线程放入锁池(lock pool)中
3.其他阻塞
运行(running)的线程执行Thread.sleep(long ms)或join()方法,或者发出了I/O请求时,JVM会把该线程置为阻塞状态。当sleep()状态超时、join()等待线程终止或者超时、或者I/O处理完毕时,线程重新自动转入可运行(runnable)状态
- 销毁
线程完成了它的全部工作或线程被提前强制性地中止或出现异常导致结束
2.2常用命令
- Thread类
void start();//启动线程
void sleep(long);//让线程进入睡眠,让出cpu资源,不释放锁
void yield();//让步,让出CPU资源,给予同级别和更高优先级的线程获取资源的机会,也可能自己再次获取资源
void interrupt();//设置中断标识为true
void interrupted();//第一次调用将中断标识设置为true,第二次设置为false
void isInterrupted();//判断中断标识是否为true
void join();//主线程等待调用此方法的线程执行结束
void join(long);//主线程等待调用此方法的线程执行结束/超时 - Obecjt类
void wait();//让线程进入等待,让出CPU资源,释放锁
void wait(long);
void notify();//唤醒一个(随机)在此对象监视器上等待的线程
void notifyAll();//唤醒所有在此对象监视器上等待的线程
2.3调度方式
- 时间片:所有线程轮流使用 CPU 的使用权,平均分配每个线程占用 CPU 的时间。
- 抢占式:优先让优先级高的线程使用 CPU,如果线程的优先级相同,那么会随机选择一个(线程随机性),Java使用的为抢占式调度。
2.4优先级
- 等级
MAX_PRIORITY:10
MIN_PRIORITY:1
NORM_PRIORITY:5 - 方法
getPriority()//返回线程优先级
setPriority(int newPriority)//改变线程的优先级
2.5分类
- 用户线程
是用户创建的一般线程,如继承Thread类或实现Runnable接口等实现的线程。 - 守护线程
是为用户线程提供服务的线程,如JVM的垃圾回收、内存管理等线程。
区别:当一个用户线程结束后,JVM会检查系统中是否还存在其他用户线程,如果存在则按照正常的调用方法调用。但是如果只剩守护线程而没有用户线程的话,JVM就会终止(从始至终都没有理睬守护线程)。
任何线程都可以是守护线程或者用户线程,所有线程一开始都是用户线程。
方法:Thread.setDaemon(false/true)设置为用户线程/守护线程;如果不设置该属性,默认为false。
需要注意的是:setDaemon()方法仅仅在线程对象已经被创建但是还没有运行前才能被调用,否则会报错
2.6优点
- 内存共享
进程间不能共享内存,但线程之间可以共享内存非常容易。 - 效率高,不需要重新分配资源
系统创建进程需要为该进程重新分配系统资源,但创建线程则代价小的多,因此使用多线程来实现多任务并发比多进程的效率高。 - java支持
Java语言内置多线程功能支持,而不是单纯地作为底层操作系统的调度方式,从而简化了Java的多线程编程。
2.7特性
- 原子性
一个或多个操作执行过程中不可被打断
由于线程的切换,导致多个线程同时执行同一段代码,带来的原子性问题。(时间片轮转) - 可见性
一个线程对共享变量的修改,另外一个线程能够立刻看到。
缓存不能及时刷新导致了可见性问题。 - 有序性
程序执行的顺序按照代码先后顺序有序执行
编译器为了优化性能而改变程序中语句的先后顺序,导致有序性问题。
三.创建线程
常用的有通过继承于Thread类,实现Runable接口方式,实现callable接口方式
- 继承于Thread类
1.创建一个集成于Thread类的子类
2.重写Thread类的run()方法
3.创建Thread子类的对象
4.通过此对象调用start()方法
- 实现Runable接口方式
1.创建一个实现了Runable接口的类
2.实现类去实现Runnable中的抽象方法:run()
3.创建实现类的对象
4.将此对象作为参数传递到Thread类中的构造器中,创建Thread类的对象
5.通过Thread类的对象调用start()
- 实现callable接口
1.创建一个实现callable的实现类
2.实现call方法,将此线程需要执行的操作声明在call()中
3.创建callable实现类的对象
4.将callable接口实现类的对象作为传递到FutureTask的构造器中,创建FutureTask的对象
5.将FutureTask的对象作为参数传递到Thread类的构造器中,创建Thread对象,并调用start方法启动
补充
开发中,优先选择实现Runable接口的方式
1:实现的方式没有类的单继承性的局限性
2:实现的方式更适合用来处理多个线程有共享数据的情况
联系:Thread也是实现自Runable,两种方式都需要重写run()方法,将线程要执行的逻辑声明在run中
与使用runnable方式相比,callable功能更强大些:
runnable重写的run方法不如callaalbe的call方法强大,call方法可以有返回值
方法可以抛出异常
支持泛型的返回值
需要借助FutureTask类,比如获取返回结果
如果任务不需要返回结果或抛出异常推荐使用Runnable接口,这样代码看起来会更加简洁。
四.线程池
4.1定义
线程池其实就是一种多线程处理形式,处理过程中可以将任务添加到队列中,然后在创建线程后自动启动这些任务。
4.2作用
线程池主要用来解决线程生命周期开销问题和资源不足问题。通过对多个任务重用线程,线程创建的开销就被分摊到了多个任务上了,而且由于在请求到达时线程已经存在,所以消除了线程创建所带来的延迟。这样,就可以立即为请求服务,使应用程序响应更快。另外,通过适当地调整线程池中的线程数目可以防止出现资源不足的情况。
4.3创建线程池
- 使用ThreadPoolExecutor 创建线程池
需要创建实现runnable或者callable接口方式的对象
创建executorservice线程池
将创建好的实现了runnable接口类的对象放入executorService对象的execute方法中执行。
关闭线程池
补充:Executors 返回线程池对象的弊端如下:
FixedThreadPool和SingleThreadExecutor:允许请求的队列长度为Integer.MAX_VALUE,可能堆积大量的请求,从而导致 OOM
CachedThreadPool 和 ScheduledThreadPool:允许请求的队列长度为Integer.MAX_VALUE,可能创建大量线程,从而导致 OOM
4.4优点
- 降低系统资源消耗
通过重用已存在的线程,降低线程创建和销毁造成的消耗; - 提高系统响应速度
当有任务到达时,通过复用已存在的线程,无需等待新线程的创建便能立即执行; - 方便线程并发数的管控
因为线程若是无限制的创建,可能会导致内存占用过多而产生OOM,并且会造成cpu过度切换(cpu切换线程是有时间成本的(需要保持当前执行线程的现场,并恢复要执行线程的现场)) - 提供更强大的功能
延时定时线程池。
4.5分类
- newSingleThreadExecutor
创建一个单线程的线程池。这个线程池只有一个线程在工作,也就是相当于单线程串行执行所有任务。如果这个唯一的线程因为异常结束,那么会有一个新的线程来替代它。此线程池保证所有任务的执行顺序按照任务的提交顺序执行。
不建议使用:FixeThreadPool使用无界队列LinkedBlockingQueue(队列的容量为 Intger.MAX_VALUE)作为线程池的工作队列,不会存在拒绝任务,只会将任务添加到等待队列中,可能会导致OOM。
- newFixeThreadPool
创建固定大小的线程池。每次提交一个任务就创建一个线程,直到线程达到线程池的最大大小。线程池的大小一旦达到最大值就会保持不变,如果某个线程因为执行异常而结束,那么线程池会补充一个新线程。
1.如果当前运行的线程数小于 corePoolSize,如果再来新任务的话,就创建新的线程来执行任务;
2. 当前运行的线程数等于 corePoolSize 后,如果再来新任务的话,会将任务加入LinkedBlockingQueue
3. 线程池中的线程执行完手头的任务后,会在循环中反复从LinkedBlockingQueue中获取任务来执行
- newCachedThreadPool
创建一个可缓存的线程池。如果线程池的大小超过了处理任务所需要的线程,那么就会回收部分空闲(60秒不执行任务)的线程,当任务数增加时,此线程池又可以智能的添加新线程来处理任务。此线程池不会对线程池大小做限制,线程池大小完全依赖于操作系统(或者说JVM)能够创建的最大线程大小。
CachedThreadPool的实现:CachedThreadPool的corePoolSize被设置为空(0),maximumPoolSize被设置为Integer.MAX.VALUE,即它是无界的,这也就意味着如果主线程提交任务的速度高于maximumPool中线程处理任务的速度时,CachedThreadPool会不断创建新的线程。极端情况下,这样会导致耗尽cpu 和内存资源,CachedThreadPool允许创建的线程数量为 Integer.MAX_VALUE ,可能会创建大量线程,从而导致OOM
- newScheduledThreadPool
创建一个大小无限的线程池。此线程池支持定时以及周期性执行任务的需求。
4.6参数及配置
核心线程数(corePoolSize)
核心运行的poolSize,也就是当超过这个范围的时候,就需要将新的Runnable放入到等待队列workQueue中了;核心线程数的设计需要依据任务的处理时间和每秒产生的任务数量来确定
最大线程数(maximumPoolSize)
当大于了这个值就会将任务由一个丢弃处理机制来处理,但是当你发生:newFixedThreadPool的时候,corePoolSize和maximumPoolSize是一样的,而corePoolSize是先执行的,所以他会先被放入等待队列,而不会执行到下面的丢弃处理中,最大线程数的设计除了需要参照核心线程数的条件外,还需要参照系统每秒产生的最大任务数决定
最大空闲时间(keepAliveTime)
默认都是0,当线程没有任务处理后,保持多长时间,当你使用:newCachedThreadPool(),它将是60s的时间。这个参数的设计完全参考系统运行环境和硬件压力设定,没有固定的参考值
单位(unit)
keepAliveTime 时长对应的单位。
任务阻塞队列(workQueue)
- 作用
等待队列,当达到corePoolSize的时候,就向该等待队列放入线程信息
任务队列长度一般设计为:核心线程数/单个任务执行时间*2即可 - 分类
- ArrayBlockingQueue
规定大小的BlockingQueue,其构造必须指定大小。其所含的对象是FIFO顺序排序的。
使用lock独占锁,来保证并发操作的安全。
入队操作
put等放入操作,首先是获取锁,如果发现数据满了,就通过notFull的condition,来阻塞线程。
阻塞调用时,唤醒条件为超时或者队列非满
进队成功后,执行notempty.signa()唤起被阻塞的出队线程
- LinkedBlockingQueue
大小不固定的BlockingQueue,若其构造时指定大小,生成的BlockingQueue有大小限制,不指定大小,其大小有Integer.MAX_VALUE来决定。其所含的对象是FIFO顺序排序的。
- synchronousQueue
特殊的BlockingQueue,对其的操作必须是放和取交替完成
线程工厂(threadFactory)
用来为线程池创建线程,当我们不指定线程工厂时,线程池内部会调用
拒绝策略(handler)
当线程池的缓存队列已满并且线程池中的线程数目达到maximumPoolSize,如果还有任务到来就会采取任务拒绝策略
ThreadPoolExecutor.AbortPolicy
丢弃任务并抛出RejectedExecutionException异常。
ThreadPoolExecutor.DiscardPolicy
也是丢弃任务,但是不抛出异常。
ThreadPoolExecutor.DiscardOldestPolicy
丢弃队列最前面的任务,然后重新尝试执行任务(重复此过程)
ThreadPoolExecutor.CallerRunsPolicy
由调用线程处理该任务
4.7任务执行流程
- 当线程池小于corePoolSize时,新提交任务将创建一个新线程执行任务,即使此时线程池中存在空闲线程。
- 当线程池达到corePoolSize时,新提交任务将被放入workQueue中,等待线程池中任务调度执行
- 当workQueue已满,且maximumPoolSize>corePoolSize时,新提交任务会创建新线程执行任务
- 当提交任务数超过maximumPoolSize时,新提交任务由RejectedExecutionHandler处理
- 当线程池中超过corePoolSize线程,空闲时间达到keepAliveTime时,关闭空闲线程
- 当设置allowCoreThreadTimeOut(true)时,线程池中corePoolSize线程空闲时间达到keepAliveTime也将关闭
4.8方法
- execute()方法用于提交不需要返回值的任务,所以无法判断任务是否被线程池执行成功与否;
- submit()方法用于提交需要返回值的任务。线程池会返回一个Future类型的对象,通过这个Future对象可以判断任务是否执行成功。
- shutdown() :关闭线程池,线程池的状态变为SHUTDOWN,线程池不再接受新任务了,但是队列里的任务得执行完毕。
- shutdownNow() :关闭线程池,线程的状态变为STOP。线程池会终止当前正在运行的任务,并停止处理排队的任务并返回正在等待执行的 List。
- isShutDown当调用shutdown()方法后返回为 true。
- isTerminated当调用shutdown()方法后,并且所有提交的任务完成后返回为 true
4.9线程池大小确定
任务判断
CPU 密集型简单理解就是利用 CPU 计算能力的任务比如你在内存中对大量数据进行排序。但凡涉及到网络读取,文件读取这类都是 IO 密集型,这类任务的特点是 CPU 计算耗费时间相比于等待 IO 操作完成的时间来说很少,大部分时间都花在了等待 IO 操作完成上。
确定大小
- CPU 密集型任务(N+1)
这种任务消耗的主要是 CPU 资源,可以将线程数设置为 N(CPU 核心数)+1,比 CPU 核心数多出来的一个线程是为了防止线程偶发的缺页中断,或者其它原因导致的任务暂停而带来的影响。一旦任务暂停,CPU 就会处于空闲状态,而在这种情况下多出来的一个线程就可以充分利用 CPU 的空闲时间。 - *I/O 密集型任务(N)
- 这种任务应用起来,系统会用大部分的时间来处理 I/O 交互,而线程在处理 I/O 的时间段内不会占用 CPU 来处理,这时就可以将 CPU 交出给其它线程使用。因此在 I/O 密集型任务的应用中,我们可以多配置一些线程,具体的计算方法是 2N 或者(线程等待时间与线程CPU时间之比 + 1) N
4.10 创建线程池
一个比较简单的线程池至少应包含线程池管理器、工作线程、任务队列、任务接口等部分。其中线程池管理器(ThreadPool Manager)的作用是创建、销毁并管理线程池,将工作线程放入线程池中;工作线程是一个可以循环执行任务的线程,在没有任务时进行等待;任务队列的作用是提供一种缓冲机制,将没有处理的任务放在任务队列中;任务接口是每个任务必须实现的接口,主要用来规定任务的入口、任务执行完后的收尾工作、任务的执行状态等,工作线程通过该接口调度任务的执行。
//参数初始化
private static final int CPU_COUNT = Runtime.getRuntime().availableProcessors();
//核心线程数量大小
private static final int corePoolSize = Math.max(2, Math.min(CPU_COUNT - 1, 4));
//线程池最大容纳线程数
private static final int maximumPoolSize = CPU_COUNT * 2 + 1;
//线程空闲后的存活时长
private static final int keepAliveTime = 30;
//任务过多后,存储任务的一个阻塞队列
BlockingQueue<Runnable> workQueue = new SynchronousQueue<>();
//线程的创建工厂
ThreadFactory threadFactory = new ThreadFactory() {
private final AtomicInteger mCount = new AtomicInteger(1);
public Thread newThread(Runnable r) {
return new Thread(r, "AdvacnedAsyncTask #" + mCount.getAndIncrement());
}
};
//线程池任务满载后采取的任务拒绝策略
RejectedExecutionHandler rejectHandler = new
ThreadPoolExecutor.DiscardOldestPolicy();
//线程池对象,创建线程
ThreadPoolExecutor mExecute = new ThreadPoolExecutor(
corePoolSize,
maximumPoolSize,
keepAliveTime,
TimeUnit.SECONDS,
workQueue,
threadFactory,
rejectHandler
);
4.11使用场景
当一个Web服务器接受到大量短小线程的请求时,使用线程池技术是非常合适的,它可以大大减少线程的创建和销毁次数,提高服务器的工作效率。但如果线程要求的运行时间比较长,此时线程的运行时间比创建时间要长得多,单靠减少创建时间对系统效率的提高不明显,此时就不适合应用线程池技术,需要借助其它的技术来提高服务器的服务效率。
适用于大量运行时间短的线程请求
4.12使用风险
虽然线程池是构建多线程应用程序的强大机制,但使用它并不是没有风险的。用线程池构建的应用程序容易遭受任何其它多线程应用程序容易遭受的所有并发风险,诸如同步错误和死锁加粗样式,它还容易遭受特定于线程池的少数其它风险,诸如与池有关的死锁、资源不足和线程泄漏。
- 死锁
任何多线程应用程序都有死锁风险。当一组进程或线程中的每一个都在等待一个只有该组中另一个进程才能引起的事件时,我们就说这组进程或线程死锁了。线程池中还存在另一种死锁,线程池中的线程等待队列中另一任务的执行结果,但这一任务却因为没有可用的线程而不能运行。
- 资源不足
相对于其它替代调度机制(有些我们已经讨论过)而言,它们通常执行得很好。但只有恰当地调整了线程池大小时才是这样的。线程消耗包括内存和其它系统资源在内的大量资源。除了 Thread 对象所需的内存之外,每个线程都需要两个可能很大的执行调用堆栈。除此以外,JVM可能会为每个 Java 线程创建一个本机线程,这些本机线程将消耗额外的系统资源。最后,虽然线程之间切换的调度开销很小,但如果有很多线程,环境切换也可能严重地影响程序的性能。如果线程池太大,那么被那些线程消耗的资源可能严重地影响系统性能。在线程之间进行切换将会浪费时间,而且使用超出比您实际需要的线程可能会引起资源匮乏问题,因为池线程正在消耗一些资源,而这些资源可能会被其它任务更有效地利用。除了线程自身所使用的资源以外,服务请求时所做的工作可能需要其它资源,例如 JDBC 连接、套接字或文件。这些也都是有限资源,有太多的并发请求也可能引起失效,例如不能分配 JDBC 连接。
并发错误
线程池和其它排队机制依靠使用 wait() 和 notify() 方法,这两个方法都难于使用。如果编码不正确,那么可能丢失通知,导致线程保持空闲状态,尽管队列中有工作要处理。使用这些方法时,必须格外小心;即便是专家也可能在它们上面出错。而最好使用现有的、已经知道能工作的实现,例如在下面的无须编写您自己的池中讨论的 util.concurrent 包。
线程泄漏
各种类型的线程池中一个严重的风险是线程泄漏,当从池中除去一个线程以执行一项任务,而在任务完成后该线程却没有返回池时,会发生这种情况。发生线程泄漏的一种情形出现在任务抛出一个RuntimeException 或一个 Error 时。如果池类没有捕捉到它们,那么线程只会退出而线程池的大小将会永久减少一个。当这种情况发生的次数足够多时,线程池最终就为空,而且系统将停止,因为没有可用的线程来处理任务。有些任务可能会永远等待某些资源或来自用户的输入,而这些资源又不能保证变得可用,用户可能也已经回家了,诸如此类的任务会永久停止,而这些停止的任务也会引起和线程泄漏同样的问题。如果某个线程被这样一个任务永久地消耗着,那么它实际上就被从池除去了。对于这样的任务,应该要么只给予它们自己的线程,要么只让它们等待有限的时间。
请求过载
仅仅是请求就压垮了服务器,这种情况是可能的。在这种情形下,我们可能不想将每个到来的请求都排队到我们的工作队列,因为排在队列中等待执行的任务可能会消耗太多的系统资源并引起资源缺乏。在这种情形下决定如何做取决于您自己;在某些情况下,您可以简单地抛弃请求,依靠更高级别的协议稍后重试请求,您也可以用一个指出服务器暂时很忙的响应来拒绝请求。
4.13使用准则
- 不要对那些同步等待其它任务结果的任务排队
这可能会导致上面所描述的那种形式的死锁,在那种死
锁中,所有线程都被一些任务所占用,这些任务依次等待排队任务的结果,而这些任务又无法执行,因
为所有的线程都很忙。 - 在为时间可能很长的操作使用合用的线程时要小心
这可能会导致上面所描述的那种形式的死锁,在那种死锁中,所有线程都被一些任务所占用,这些任务依次等待排队任务的结果,而这些任务又无法执行,因为所有的线程都很忙。 - 理解任务
要有效地调整线程池大小,您需要理解正在排队的任务以及它们正在做什么。它们是 CPU 限制的(CPU-bound)吗?它们是 I/O 限制的(I/O-bound)吗?您的答案将影响您如何调整应用程序。如果您有不同的任务类,这些类有着截然不同的特征,那么为不同任务类设置多个工作队列可能会有意义,这样可以相应地调整每个池。 - 调整池的大小
调整线程池的大小基本上就是避免两类错误:线程太少或线程太多。线程池的最佳大小取决于可用处理器的数目以及工作队列中的任务的性质。 - 使用已有的池
Doug Lea 编写了一个优秀的并发实用程序开放源码库 util.concurrent,它包括互斥、信号量、诸如在并发访问下执行得很好的队列和散列表之类集合类以及几个工作队列实现。该包中的 PooledExecutor类是一种有效的、广泛使用的以工作队列为基础的线程池的正确实现。您无须尝试编写您自己的线程池,这样做容易出错,相反您可以考虑使用 util.concurrent 中的一些实用程序。util.concurrent 库也激发了 JSR 166,JSR 166 是一个 Java 社区过程(Java Communi)
4.14如何等待子线程执行结束
- 主动式指主线主动去检测某个标志位, 判断子线程是否已经完成.
被动式指主线程被动的等待子线程的结
束, 很明显, 比较符合人们的胃口. 就是你事情做完了,Thread 类给我们提供了join 系列的方法, 这些方法的目的就是等待当前线程的die
- 使用并发包下面的Future模式
Future是一个任务执行的结果, 他是一个将来时, 即一个任务执行, 立即异步返回一个Future对象, 等到任
务结束的时候, 会把值返回给这个future对象里面. 我们可以使用ExecutorService接口来提交一个线程.
五.对象
java对象有3部分构成,分别为对象头,实例数据,对齐填充
对象头
一般由MarkWord头信息,KlassPoin类型指针,数组长度(可选)组成。
- mark word
存储对象hashcode,分代年龄,和锁标志位等信息 - Klass point
存储对象的类型指针,该指针指向它的类元数据,JVM通过这个指针确定对象是哪个类的实例 - 数组长度
如果对象是一个数组,那么对象头还需要有额外的空间用于存储数组的长度
实例数据
实例数据就是对象真正存储的有效信息,也就是代码中定义的各种类型的字段内容,不论是父类的还是子类的,都需要记录下来。其存储顺序受到虚拟机分配策略和定义顺序影响。
对齐填充
对象填充不是必要数据。在模型中只是起到占位符的作用。
整个对象都要以8字节为基准,比如前面为12字节,那对齐填充就是4字节,加起来是16,可以被8整除,至于为什么,因为操作系统总线带宽是以8字节为基础传输,所以提高传输效率之类的巴拉巴拉。。。
六.锁
6.1分类
- 可重入锁
如果锁具备可重入性,则称作为可重入锁
synchronized和Lock都具备可重入性
基于线程的分配,而不是基于方法调用的分配
当一个线程执行到某个synchronized方法时,比如说method1,而在method1中会调用另外一个synchronized方法method2,此时线程不必重新去申请锁,而是可以直接执行方法method2。
- 可中断锁
可以响应中断的锁
如果某一线程A正在执行锁中的代码,另一线程B正在等待获取该锁,可能由于等待时间过长,线程B不想等待了,想先处理其他事情,我们可以让它中断自己或者在别的线程中中断它,这种就是可中断锁。
synchronized就不是可中断锁,而Lock是可中断锁
- 公平锁
公平锁即尽量以请求锁的顺序来获取锁
比如同是有多个线程在等待一个锁,当这个锁被释放时,等待时间最久的线程(最先请求的线程)会获得该所,这种就是公平锁
- 非公平锁
无法保证锁的获取是按照请求锁的顺序进行的
可能导致某个或者一些线程永远获取不到锁
synchronized就是非公平锁,而对于ReentrantLock和ReentrantReadWriteLock,它默认情况下是非公平锁,但是可以设置为公平锁。
- 读写锁
读写锁将对一个资源(比如文件)的访问分成了2个锁,一个读锁和一个写锁,使得多个线程之间的读操作不会发生冲突
ReadWriteLock就是读写锁,它是一个接口,ReentrantReadWriteLock实现了这个接口,可以通过readLock()获取读锁,通过writeLock()获取写锁。
6.2死锁
定义
死锁是指多个线程因竞争资源而造成的一种僵局(互相等待),若无外力作用,这些进程都将无法向前推进。
产生原因
因争夺资源而陷入僵局
必要条件
- 互斥条件(独占资源)
在一段时间内某资源仅为一个进程所占有 - 不剥夺条件(不可抢夺)
进程所获得的资源在未使用完毕之前,不能被其他进程强行夺走 - 请求和保持条件(阻塞保持)
进程已经保持了至少一个资源,但又提出了新的资源请求,而该资源 已被其他进程占有,此时请求进程被阻塞,但对自己已获得的资源保持不放 - 循环等待条件(资源传递)
存在一种进程资源的循环等待链,链中每一个进程已获得的资源同时被链中下一个进程所请求
解决方案
1.减少同步共享变量
2.采用专门的算法,多个线程之间规定先后执行的顺序,规避死锁问题
3.减少锁的嵌套。
七.同步
Java允许多线程并发控制,当多个线程同时操作一个可共享资源变量时(如数据的增删改查),将会导致数据不准确,相互之间产生冲突,因此加入同步锁以避免在该线程没有完成操作之前,被其他线程的调用,从而保证了该变量的唯一性和准确性。
7.1synchronized
原理
jVM 是通过进入、退出 对象监视器(Monitor) 来实现对方法、同步块的同步的,而对象监视器的本质依赖于底层操作系统的 互斥锁(Mutex Lock) 实现。具体实现是在编译之后在同步方法调用前加入一个monitor.enter指令,在退出方法和异常处插入monitor.exit的指令。对于没有获取到锁的线程将会阻塞到方法入口处,直到获取锁的线程monitor.exit之后才能尝试继续获取锁。
作用
保证同一时刻最多只有1个线程执行被Synchronized修饰的方法 / 代码,其他线程 必须等待当前线程执行完该方法 / 代码块后才能执行该方法 / 代码块
同步代码快
JVM采用monitorenter、monitorexit两个指令来实现同步。
可以把执行monitorenter指令理解为加锁,执行monitorexit理解为释放锁。 每个对象维护着一个记录着被锁次数的计数器。未被锁定的对象的该计数器为0,当一个线程获得锁(执行monitorenter)后,该计数器自增变为 1 ,当同一个线程再次获得该对象的锁的时候,计数器再次自增。当同一个线程释放锁(执行monitorexit指令)的时候,计数器再自减。当计数器为0的时候。锁将被释放,其他线程便可以获得锁。
同步方法
JVM采用ACC_SYNCHRONIZED标记符来实现同步
方法级的同步是隐式的。同步方法的常量池中会有一个ACC_SYNCHRONIZED标志。当某个线程要访问某个方法的时候,会检查是否有ACC_SYNCHRONIZED,如果有设置,则需要先获得监视器锁,然后开始执行方法,方法执行之后再释放监视器锁。这时如果其他线程来请求执行方法,会因为无法获得监视器锁而被阻断住。值得注意的是,如果在方法执行过程中,发生了异常,并且方法内部并没有处理该异常,那么在异常被抛到方法外面之前监视器锁会被自动释放。
锁升级
- 过程
无锁(锁对象初始化时)-> 偏向锁(有线程请求锁) -> 轻量级锁(多线程轻度竞争)-> 重量级锁(线程过多或长耗时操作,线程自旋过度消耗cpu); - 分类
偏向锁
当对象被当做同步锁并有一个线程A抢到了锁时,锁标志位还是01,但是否偏向锁那一位改成1,前23bit记录抢到锁的线程id,表示进入偏向锁状态
轻量级锁(自旋锁)
偏向锁状态抢锁失败,代表当前锁有一定的竞争,偏向锁将升级为轻量级锁。
重量级锁
自旋锁重试之后如果抢锁依然失败,同步锁会升级至重量级锁
补充
锁只能升级,不能降级;偏向锁可以被重置为无锁状态
偏向锁和轻量级锁在用户态维护,重量级锁需要切换到内核态(os)进行维护
锁的升级通过CAS的自旋来进行判断升级
7.2Lock
实现原理
Lock完全用Java写成,在java这个层面是无关JVM实现的。
lock由int类型的状态值(用于锁的状态更新),一个双向链表构成(用于存储等待中的线程)。通过CAS获取状态值修改,如果没有获取到,就放到等待链表中,释放时修改状态值,调整等待链表。
lock大量使用CAS+自旋。因此根据CAS特性,lock建议使用在低锁冲突的情况下。目前java1.6以后,官方对synchronized做了大量的锁优化(偏向锁、自旋、轻量级锁)。因此在非必要的情况下,建议使用synchronized做同步操作。
作用
为了解决synchronized中线程被阻塞,却没释放锁而影响执行效率。
获取锁
AQS
与synchronized的对比
Lock是一个接口,而synchronized是Java中的关键字,synchronized是内置的语言实现;
synchronized在发生异常时,会自动释放线程占有的锁,因此不会导致死锁现象发生;而Lock在发生异常时,如果没有主动通过unLock()去释放锁,则很可能造成死锁现象,因此使用Lock时需要在finally块中释放锁;
Lock可以让等待锁的线程响应中断,而synchronized却不行,使用synchronized时,等待的线程会一直等待下去,不能够响应中断;
通过Lock可以知道有没有成功获取锁,而synchronized却无法办到。
Lock可以提高多个线程进行读操作的效率。
常用方法
- lock()
获取锁,在finally主动释放锁,防止死锁 - tryLock()
尝试一次获取锁,并返回是否获取到锁 - tryLock(long time, TimeUnit unit)
在一定时间内尝试获取锁,并返回是否获取到锁 - lockInterruptibly()
可打断的获取锁,可以打断正在等待锁的线程
ReentrantLock
可重入锁,ReentrantLock是唯一实现了Lock接口的类
ReadWriteLock(接口)
- 方法
Lock readLock()
Lock writeLock();
一个用来获取读锁,一个用来获取写锁。也就是说将文件的读写操作分开,分成2个锁来分配给线程,从而使得多个线程可以同时进行读操作
- 实现类
ReentrantReadWriteLock
多个线程可以一起读
读写互斥
7.3Volatile
作用
保证了不同线程对这个变量进行操作时的可见性;禁止进行指令重排序
实现原理
- 保证可见性
使用Volatile修饰,会强制将所修饰的变量写入主存,当该变量被修改时,会将所有缓存中的该变量设置无效,如果其他线程访问会去主存中读取
- 原子性
通过synchronized或者Lock或者AtomicInteger来实现;自增操作不是原子性操作,而且volatile也无法保证对变量的任何操作都是原子性的
- 实现有序性
通过内存屏障实现
当程序执行到volatile变量的读操作或者写操作时,在其前面的操作的更改肯定全部已经进行,且结果已经对后面的操作可见;在其后面的操作肯定还没有进行;在进行指令优化时,不能将在对volatile变量访问的语句放在其后面执行,也不能把volatile变量后面的语句放到其前面执行
7.4ThreadLocal
定义
ThreadLocal是除了加锁这种同步方式之外的一种保证一种规避多线程访问出现线程不安全的方法
原理
每个线程持有一个 Map 并维护了 ThreadLocal 对象与具体实例的映射,该 Map 由于只被持有它的线程访问,故不存在线程安全以及锁的问题
作用
并不解决线程间共享数据的问题,ThreadLocal 通过隐式的在不同线程内创建独立实例副本避免了实例线程安全的问题
问题
- 内存泄露
ThreadLocalMap 的 Entry 对 ThreadLocal 的引用为弱引用,避免了 ThreadLocal 对象无法被回收的问题
ThreadLocalMap 的 set 方法通过调用 replaceStaleEntry 方法回收键为 null 的 Entry 对象的值(即为具体实例)以及 Entry 对象本身从而防止内存泄漏
适用情况
ThreadLocal 适用于变量在线程间隔离且在方法间共享的场景
7.5CountDownLatch
定义
CountDownLanch 是一个倒数计数器, 给一个初始值(>=0), 然后没countDown一次就会减1, 这很符合等待多个子线程结束的产景: 一个线程结束的时候, countDown一次, 直到所有都countDown了 , 那么所有子线程就都结束了,
方法
await: 会阻塞等待计数器减少到0位置. 带参数的await是多了等待时间.
countDown: 将当前的技术减1
getCount(): 返回当前的计数
实现
我们只需要在子线程执行之前, 赋予初始化countDownLanch, 并赋予线程数量为初始值.每个线程执行完毕的时候, 就countDown一下.主线程只需要调用await方法, 可以等待所有子线程执行结束。