目录
2.当线程并发执行时,由于资源共享和线程协作,使用线程之间会存在以下两种制约关系
1.代码演示部分一(主线程执行一次副线程执行一次,交替执行五次)
2.代码演示部分二(用多线程交替输出最后结果为A1,B2,B3....Z26)
4)wait notify notifyall join sleep yiedld
一.传统使用类Thread 和接口Runnable实现
1)创建一个类继承thread子类,并重写run方法即可
2)实现runnable接口
采用了匿名内部类,在thread中需要一个接口Runnable的对象,而匿名内部类的作用就是帮助接口Runnable产生 一个这个接口的实现类;
简单的说,我要用这个接口中的方法,但是不能被我实例化,那我就用内部类实现这个接口
MyRunnable target = new MyRunnable();
Thread thread = new Thread(target);
thread.start();//开启线程
3)总结:
继承Thread类
优点: 编写简单,如果需要访问当前线程,无需使用Thread.currentThread()方法,直接使用this即可获得当前线程
缺点: 自定义的线程类已继承了Thread类,所以后续无法再继承其他的类
实现Runnable接口
优点: 自定义的线程类只是实现了Runnable接口或Callable接口,后续还可以继承其他类,在这种方式下,多个线程可以共享同一个target对象,所以非常适合多个相同线程来处理同一份资源的情况,从而可以将CPU、代码、还有数据分开(解耦),形成清晰的模型,较好地体现了面向对象的思想
缺点: 编程稍微复杂,如想访问当前线程,则需使用Thread.currentThread()方法
2.当线程并发执行时,由于资源共享和线程协作,使用线程之间会存在以下两种制约关系
1)间接相互制约。一个系统中的多个线程必然要共享某种系统资源,如打印机....线程 A 在使用打印机时,其它线程都要等待。(特殊的同步)
2)直接相互制约。这种制约主要是因为线程之间的合作,如有线程 A 将计算结果提供给线程 B 作进一步处理,那么线程 B 在线程 A 将数据送达之前都将处于阻塞状态。(同步)
1.代码演示部分一(主线程执行一次副线程执行一次,交替执行五次)
=======================================================================
public class Test {
public static void main(String[] args) {
final Bussiness bussiness=new Bussiness(); //finanl修饰对象,地址不能改变
//创建一个子线程,run方法里面子线程执行3变
new Thread(new Runnable() {
public void run() {
for(int i=0;i<3;i++) {
bussiness.subMethod();
}
}
}).start(); //start()方法是启动多线线程的方法,但是此时run方法里的代码又称线程体是处于就绪状态,所以会先执行下面的代码,即执行主线程的代码.
//主线程
for(int i=0;i<3;i++) {
bussiness.mainMethod();
}
}
}
class Bussiness{
//创建一个私有的标识位
private boolean subFlag=true;
public synchronized void mainMethod() {
while(subFlag) { //如果标识为ture,执行for循环里的代码
for (int i = 0; i < 5; i++) {
System.out.println("this is main of " + i);
}
flag = false;//防止这个线程连续执行两次
//唤醒另外一个等待的线程
this.notify();
}
try {
//让当前的等待,知道其他对象调用此对象的notify方法或notifyAll()方法
this.wait();//阻塞
} catch (InterruptedException e) {
e.printStackTrace();
}
}
public synchronized void subMethod() {
/*
* 有时候一种叫做伪唤醒的情况,这时候如果用if就不会再判断了,就会往下运行,这时候就可能会出错
* 但是如果用while就算是出现伪唤醒还是会去判断就不会出错。这里用while体现了你的水平
*/
while(!subFlag)
{
for (int i = 0; i < 5; i++) {
System.out.println("this is sub of " + i);
}
flag = true;
this.notify();
}
try {
this.wait();//这里等待,知道被唤醒,被唤醒的时候其实就是回到了这行代码这里
} catch (InterruptedException e) {
e.printStackTrace();
}
}
}
=========================================================================
2.代码演示部分二(用多线程交替输出最后结果为A1,B2,B3....Z26)
=========================================================================
public class Test1 {
public static void main(String[] args) {
ExecutorService executorService = Executors.newCachedThreadPool();
char character = 'A';
List<Character> list = new ArrayList();
for (int i = 1; i < 27; i++) {
char c = character++;
list.add(c);
}
final Object o = new Object();
new Thread(new Runnable() {
@Override
public void run() {
for (int i = 0; i < 26; i++) {
synchronized (o) {
System.out.print(list.get(i));
o.notify();
try {
o.wait();
} catch (InterruptedException e) {
e.printStackTrace();
}
}
}
}
}).start();
new Thread(new Runnable() {
@Override
public void run() {
for (int i = 1; i < 27; i++) {
synchronized (o) {
System.out.print(i + ",");
o.notify();
try {
o.wait();
} catch (InterruptedException e) {
e.printStackTrace();
}
}
}
}
}).start();
}
}
=========================================================================
3)总结:
1)由于start方法开启了多线程,所以先执行主线程的for循环
2)当主线程执行第一次(i=0)时,执行主方法,输出this is main of 0~9
3)执行完主方法里的for循环后,唤醒另一个线程并等待(释放所有资源交给其他线程执行)
4)副线程由于被唤醒,执行run方法里的代码,开始执行副方法里的循环
3.线程中的几种方法:
4)wait notify notifyall join sleep yiedld
notify()唤醒在此对象锁上等待的单个线程。
notifyAll()唤醒在此对象锁上等待的所有线程。
wait()让当前线程处于“等待(阻塞)状态”,“直到其他线程调用此对象的 notify() 方法或 notifyAll() 方法”,当前线程被唤醒(进入“就绪状态”)
join方法把指定的线程添加到当前线程中,可以不给参数直接thread.join(),也可以给一个时间参数,单位为毫秒thread.join(100)。事实上join方法是通过wait方法来实现的。当前线程A调用另一个线程的join()方法,当前线程A转入阻塞状态,直到线程B运行结束,线程A才由阻塞状态转为可执行状态。
sleep()方法是让程序停止运行的时间(时间是自己定义的),让出cpu给其他线程,但是它的监控状态依然保持者,当指定的时间到了又会自动苏醒,并返回到可运行状态,不是运行状态。sleep()中指定的时间是线程不会运行的最短时间。因此,sleep()方法不能保证该线程睡眠到期后就开始执行。在调用sleep()方法的过程中,线程不会释放对象锁。
yiedld()调用此方法,会使线程放弃当前分的CPU时间片,但此时线程仍然处于可执行状态,随时可以再次分得CPU时间片。yield()方法只能使同优先级的线程有执行的机会。调用yield()的效果等价于调度程序认为该线程已执行了足够的时间从而转到另一个线程。(暂停当前线程,礼让其他线程,且不一定成功)
二.无并发不java
1.原理:
同步代码块:
反编译可以看到monitorenter,monitorexit指令(相对于不加synchronized多出来);
原理:每个对象有一个监视器锁(monitor),当monitor被占用时就会处于锁定状态,
线程执行monitorenter指令时尝试获取monitor的所有权,过程如下:
1、monitor的进入数为0,则该线程进入monitor,将进入数设置为1,该线程即为monitor的所有者。
2、线程已经占有该monitor,只是重新进入,则进入monitor的进入数加1.
3、其他线程占用monitor,则该线程进入阻塞状态,直到monitor进入数为0,再尝试monitor所有权。
执行monitorexit的线程必须是objectref所对应的monitor的所有者。
执行后,monitor进入数减1,如果减1后进入数为0,则线程退出monitor,不再是该monitor所有者。
其他被这个monitor阻塞的线程可以尝试去获取这个monitor的所有权。
总结:Synchronized的语义底层是通过一个monitor的对象来完成,
wait/notify等方法也依赖于monitor对象,故只有在同步的块或者方法中能调用wait/notify等方法,
否则会抛出java.lang.IllegalMonitorStateException的异常。
同步方法:
原理:不直接通过指令monitorenter和monitorexit来完成;
常量池中多了ACC_SYNCHRONIZED标示符;
方法调用时,先检查方法的ACC_SYNCHRONIZED访问标志是否被设置,
如果设置了,执行线程将先获取monitor,获取成功之后才能执行方法体,方法执行完后再释放monitor;
本质上与上面没有区别,只是方法的同步是一种隐式的方式来实现,无需通过字节码来完成。
2.接口 Lock
ReentrantLock是唯一实现了Lock接口的类;
lock()、tryLock()、tryLock(long time, TimeUnit unit)和lockInterruptibly()用来获取锁;
unLock()方法用来释放锁。
原理:AbstractQueuedSynchronizer会把所有的请求线程构成一个CLH队列,
当一个线程执行完毕(lock.unlock())时会激活自己的后继节点,但正在执行的线程并不在队列中,
而那些等待执行的线程全部处于阻塞状态 。
lock源码:
final void lock() {
if (compareAndSetState(0, 1))
setExclusiveOwnerThread(Thread.currentThread());
else
acquire(1);
}
当前线程会首先尝试获得锁而不是在队列中进行排队等候,
这对于那些已经在队列中排队的线程来说显得不公平,这也是非公平锁的由来;
对于刚来竞争的线程首先会通过CAS设置状态,如果设置成功那么直接获取锁,执行临界区的代码,
反之调用acquire(1)进入同步队列中。
如果已经存在Running线程,那么CAS肯定会失败,则新的竞争线程会通过CAS的方式被追加到队尾。
总结:
1、调用lock方法,会先进行cas操作看可否设置同步状态1成功,成功则执行临界区代码(抢锁成功)
2、如果不成功获取同步状态,如果状态是0那么cas设置为1(没有有锁的其他线程)
3、如果同步状态既不是0也不是自身线程持有会把当前线程构造成一个节点(其他线程有锁)
4、把当前线程节点CAS的方式放入队列中,行为上线程阻塞,内部自旋获取状态(内部无限循环)
5、线程释放锁,唤醒队列第一个节点,参与竞争。重复上述。
=================================================================
补充理解:
在Java并发包中提供了两种类型的队列,非阻塞队列与阻塞队列;
这里的非阻塞与阻塞在于有界与否,是在初始化时是否赋予默认的容量大小;
阻塞有界队列,当队列满了,则任何线程都会阻塞不能进行入队操作,反之队列为空,则不能进行出队操作;
非阻塞无界队列不会出现队列满或者队列空的情况;
两种都保证线程的安全性,即不能有一个以上的线程同时对队列进行入队或者出队操作。
==================================================================
3.lock与synchronized的区别
Lock是一个接口,而synchronized是Java中的关键字,synchronized是内置的语言实现;
synchronized不需要用户去手动释放锁,Lock则必须要用户去手动释放锁;
synchronized在发生异常时,会自动释放线程占有的锁,不会导致死锁现象发生;
Lock在发生异常时,如没有通过unLock()去释放锁,则可能造成死锁现象,一般在finally块中释放锁;
Lock可以让等待锁的线程响应中断,synchronized不行(只能一直等待,不能中断等待的线程);
通过Lock可以知道有没有成功获取锁,synchronized不行;
Lock可以提高多个线程进行读操作的效率;
Lock可以读写分离(读与读不影响)
竞争资源不激烈,两者性能差不多,竞争资源非常激烈时,Lock的性能要远远优于synchronized。
4.可重入锁
如果锁具备可重入性,则称作为可重入锁;
基于线程的分配,而不是基于方法调用的分配;
synchronized和ReentrantLock都是可重入锁;
如:线程调用synchronized修饰的method1(其实锁住的为类的实例对象),
method1中调用synchronized修饰的method2时不用重新申请锁(基于线程分配锁)
5.可中断锁
可以相应中断的锁;
synchronized不是可中断锁,Lock是可中断锁;
如:中断等待线程
6.公平锁
公平锁即尽量以请求锁的顺序来获取锁;
synchronized是非公平锁,ReentrantLock和ReentrantReadWriteLock默认非公平,可设置成公平;
ReentrantLock lock = new ReentrantLock(true) 公平锁;
7.读写锁
读写锁将对一个资源(比如文件)的访问分成了2个锁,一个读锁和一个写锁;
线程之间的读操作不会发生冲突;
ReadWriteLock。
8.乐观锁和悲观锁
悲观锁:别人想拿这个数据就会阻塞直到自己拿到锁;
乐观锁:不上锁,但在更新的时候会判断一下在此期间别人有没有去更新这个数据。
9.CAS操作
CAS操作是乐观锁;
有的CAS操作都是Unsafe类来实现的,且都为native方法(java调用非java方法,java有局限性);
demo: CAS(V,E,N)
V表示要更新的变量
E表示预期值
N表示新值
如果V值等于E值,则将V的值设为N。若V值和E值不同,则说明已经有其他线程做了更新,则当前线程什么都不做;
10.volatile 关键字
Java提供了volatile关键字来保证可见性;
共享变量被volatile修饰,会保证修改的值会立即被更新到主存,其他线程读取时,会去主存中读取新值;
普通共享变量被修改之后,什么时候被写入主存是不确定的;
如,当线程2进行修改,会导致线程1工作内存中缓存变量无效,只能去主存读取(没有则等待;
对禁止指令重排序有一定作用,不能将在对volatile变量访问的语句放在其后面执行,前面也一样;(写操作优先于读操作)
原理:volatile关键字时,会多出一个lock前缀指令,相当于内存屏障。
三.线程池
1.理解线程池
Java中开辟出了一种管理线程的概念,这个概念叫做线程池,从概念以及应用场景中,我们可以看出,线程池的好处,就是可以方便的管理线程,也可以减少内存的消耗。
那么,我们应该如何创建一个线程池那?Java中已经提供了创建线程池的一个类:Executor
而我们创建时,一般使用它的子类:ThreadPoolExecutor.
ThreadPoolExecutor继承了AbstractExecutorService,
AbstractExecutorService是一个抽象类,它实现了ExecutorService接口,
ExecutorService又是继承了Executor接口.
1.corePoolSize就是线程池中的核心线程数量,这几个核心线程,只是在没有用的时候,也不会被回收,
2.maximumPoolSize就是线程池中可以容纳的最大线程的数量,
3.keepAliveTime,就是线程池中除了核心线程之外的其他的最长可以保留的时间,因为在线程池中,除了核心线程即使在无任务的情况下也不能被清除,其余的都是有存活时间的,意思就是非核心线程可以保留的最长的空闲时间,
4.util,就是计算这个时间的一个单位,
5.workQueue,就是等待队列,任务可以储存在任务队列中等待被执行,执行的是FIFIO原则(先进先出)。
6.threadFactory,就是创建线程的线程工厂,最后一个handler,是一种拒绝策略,我们可以在任务满了之后,拒绝执行某些任务。
1)handle拒绝策略和执行流程
有四种:第一种AbortPolicy:不执行新任务,直接抛出异常,提示线程池已满
第二种DisCardPolicy:不执行新任务,也不抛出异常
第三种DisCardOldSetPolicy:将消息队列中的第一个任务替换为当前新进来的任务执行
第四种CallerRunsPolicy:直接调用execute来执行当前任务
2.常见的线程池有四种:
CachedThreadPool:可缓存的线程池,该线程池中没有核心线程,非核心线程的数量为Integer.max_value,就是无限大,当有需要时创建线程来执行任务,没有需要时回收线程,适用于耗时少,任务量大的情况。
SecudleThreadPool:周期性执行任务的线程池,按照某种特定的计划执行线程中的任务,有核心线程,但也有非核心线程,非核心线程的大小也为无限大。适用于执行周期性的任务。
SingleThreadPool:只有一条线程来执行任务,适用于有顺序的任务的应用场景。
FixedThreadPool:定长的线程池,有核心线程,核心线程的即为最大的线程数量,没有非核心线程
3.使用方法
一般不提倡直接使用ThreadPoolExecutor,而是使用Executors类中提供的几个静态方法来创建线程池:
1.newFixedThreadPool(int nThreads)
public static ExecutorService newFixedThreadPool(int nThreads) {
return new ThreadPoolExecutor(nThreads, nThreads,
0L, TimeUnit.MILLISECONDS,
new LinkedBlockingQueue<Runnable>());
}
创建固定容量大小的缓冲池;
corePoolSize=maximumPoolSize,阻塞队列使用LinkedBlockingQueue,大小为整数最大值;
新任务提交时,线程池中有空闲线程则会立即执行,如果没有,则会暂存到阻塞队列。
使用无界的LinkedBlockingQueue来存放执行的任务。当提交频繁时,可能会耗尽系统资源。
在线程池空闲时,也不会释放工作线程,还会占用一定的系统资源,需要shutdown。
2.newSingleThreadExecutor()
public static ExecutorService newSingleThreadExecutor() {
return new FinalizableDelegatedExecutorService
(new ThreadPoolExecutor(1, 1,
0L, TimeUnit.MILLISECONDS,
new LinkedBlockingQueue<Runnable>()));
}
只有一个线程的线程池,阻塞队列使用的是LinkedBlockingQueue;
若有多余的任务提交到线程池中,会被暂存到阻塞队列,待空闲时再去执行。按照先入先出的顺序执行任务。
3.newCachedThreadPool()
public static ExecutorService newCachedThreadPool() {
return new ThreadPoolExecutor(0, Integer.MAX_VALUE,
60L, TimeUnit.SECONDS,
new SynchronousQueue<Runnable>());
}
缓存线程池,缓存线程默认存活60秒。corePoolSize大小为0,核心池最大为Integer.MAX_VALUE,
阻塞队列使用SynchronousQueue。是一个直接提交的阻塞队列,
会迫使线程池增加新的线程去执行新的任务,当提交频繁处理不快时,有资源损耗风险,
没有任务执行时,当线程的空闲时间超过keepAliveTime(60秒),则工作线程将会终止被回收;
4.newScheduledThreadPool(int var0)
public static ScheduledExecutorService newScheduledThreadPool(int var0) {
return new ScheduledThreadPoolExecutor(var0);
}
定时线程池,该线程池可用于周期性地去执行任务,通常用于周期性的同步数据。