多线程总结
一、进程与线程
1.进程:系统进行资源分配和调度的一个独立单位。可以通过Runtime.exec()或ProcessBuilder的start方法创建进程
线程:线程是程序执行流的最小单位。继承Thread或实现Runnble接口
1.1 线程thread的方法:start(),stop(),run(),join() 其他线程等待,执行当前线程,直至结束,sleep() 调用该方法该线程进入等待
wait()和notify()方法属于Object的方法,wait()和sleep()的区别:wait()会释放对象锁而sleep()不会释放对象锁
1.2 线程状态:
1)新建状态:新建线程对象,并没有调用start()方法之前
2)就绪状态:调用start()方法之后线程就进入就绪状态,但是并不是说只要调用start()方法线程就马上变为当前线程,在变为当前线程之前都是为就绪状态。值得一提的是,线程在睡眠和挂起中恢复的时候也会进入就绪状态哦。
3)运行状态:线程被设置为当前线程,开始执行run()方法。就是线程进入运行状态
4)阻塞状态:线程被暂停,比如说调用sleep()方法后线程就进入阻塞状态
5)死亡状态:线程执行结束,1、run方法正常退出而自然死亡,2、一个未捕获的异常终止了run方法而使线程猝死。可以通过isAlive方法判断
1.3 线程死锁:是指两个或两个以上的线程在执行过程中,因争夺资源而造成的一种互相等待的现象,若无外力作用,它们都将无法推进下去.
活锁:活锁的线程或进程的状态是不断改变的。活锁和死锁的主要区别是前者进程的状态可以改变但是却不能继续执行
1.3.1 产生原因:
1)系统资源的竞争导致系统资源不足,以及资源分配不当,导致死锁
2)线程在运行过程中,请求和释放资源的顺序不当,会导致死锁
1.3.2 四个必要条件:只要系统发生死锁则以上四个条件至少有一个成立。
- 互斥条件:一个资源被一线程占有时,则其他线程只能等待其释放。
- 不可剥夺条件:线程所获得的资源在未使用完毕之前,不被其他线程强行剥夺,而只能由获得该资源的线程资源释放。
- 请求和保持条件:请求申请新的资源的同时,继续占用已分配到的资源。
- 循环等待条件:在发生死锁时必然存在一个线程等待队列{P1,P2,…,Pn},其中P1等待P2占有的资源,P2等待P3占有的资源,…,Pn等待P1占有的资源,形成一个线程等待环路,环路中每一个线程所占有的资源同时被另一个申请,也就是前一个线程占有后一个线程所深情地资源。
1.3.3 避免死锁:
1)加锁顺序(如果多个线程顺序地获取锁,就不会发生死锁现象)
2)加锁时限(线程尝试获取锁的时候加上一定的时限,超过时限则放弃对该锁的请求,并释放自己占有的锁)
3)死锁检测 (比如用jstack检测)
1.4 锁类型
1)可重入锁:在执行对象中所有同步方法不用再次获得锁。lock和synchronized都可重入
2)可中断锁:在等待获取锁过程中可中断。lock 可中断,synchronized不可中断
3)公平锁: 按等待获取锁的线程的等待时间进行获取,等待时间长的具有优先获取锁权利。synchronized非公平锁,lock默认非公平,new ReentrantLock(true)可设置公平
4)读写锁:对资源读取和写入的时候拆分为2部分处理,读的时候可以多线程一起读,写的时候必须同步地写
1.5 守护线程和用户线程
- Daemon的作用是为其他线程的运行提供服务,比如说GC线程
- 两者区别:如果User Thread全部撤离,那么Daemon Thread也就没啥线程好服务的了,所以虚拟机也就退出了
- 非虚拟机内部提供,用户可以自行设定守护线程,public final void setDaemon(boolean on) ,thread.setDaemon(true)必须在thread.start()之前设置,否则会跑出一个IllegalThreadStateException异常,不能把正在运行的常规线程设置为守护线程,这点与守护进程有着明显的区别,守护进程是创建后,让进程摆脱原会话的控制+让进程摆脱原进程组的控制+让进程摆脱原控制终端的控制;所以说寄托于虚拟机的语言机制跟系统级语言有着本质上面的区别
- 在Daemon线程中产生的新线程也是Daemon的,而守护进程fork()出来的子进程不再是守护进程
- 不是所有的应用都可以分配给Daemon线程来进行服务,比如读写操作或者计算逻辑。因为在Daemon Thread还没来的及进行操作时,虚拟机可能已经退出了,JRE判断程序是否执行结束的标准是所有的前台执线程行完毕了,而不管后台线程的状态
- 守护线程实际应用:web服务器中的Servlet,容器启动时后台初始化一个服务线程,即调度线程,负责处理http请求,然后每个请求过来调度线程从线程池中取出一个工作者线程来处理该请求,从而实现并发控制的目的
1.6 java.lang.Thread中有一个方法叫holdsLock()方法来检测一个线程是否拥有锁
1.7 Thread类中的yield():可以暂停当前正在执行的线程对象,让其它有相同优先级的线程执行。它是一个静态方法而且只保证当前线程放弃CPU占用而不能保证使其它线程一定能占用CPU,执行yield()的线程有可能在进入到暂停状态后马上又被执行
二、java内存模型
2.java内存模型:只是一个抽象概念。JMM定义了Java 虚拟机(JVM)在计算机内存(RAM)中的工作方式。数据存在与主内存中,少部分存在cpn寄存器中,主内存是内存共享区域,当要修改主内存中的数据时,需要将数据复制到自己的工作内存,即cpu缓存中,操作完数据后将数据flush到主内存中
2.1 通信:共享内存和消息传递
1.1 共享内存通信方式就是通过共享对象进行通信。同步是显式进行
1.2 消息传递方式就是wait()和notify()。同步是隐式进行的
2.2 共享对象的可见性:
一个CPU中的线程读取主存数据到CPU缓存,然后对共享对象做了更改,但CPU缓存中的更改后的对象还没有flush到主存,此时线程对共享对象的更改对其它CPU中的线程是不可见的。最终就是每个线程最终都会拷贝共享对象,而且拷贝的对象位于不同的CPU缓存中
解决共享对象可见性:使用java volatile关键字。volatile原理是基于CPU内存屏障指令实现的
三、volatile
3.volatile:当CPU写数据时,会发出信号通知其他CPU将该变量的缓存行置为无效状态,它就会从内存重新读取
3.1 并发编程中:原子性问题,可见性问题,有序性问题
3.1.1 原子性:即一个操作或者多个操作 要么全部执行并且执行的过程不会被任何因素打断,要么就都不执行
请分析以下哪些操作是原子性操作:
x = 10; //原子性 只有简单的读取、赋值(而且必须是将数字赋值给某个变量,变量之间的相互赋值不是原子操作) 才是原子操作。
y = x; //非原子性
x++; //非原子性
x = x + 1; //非原子性
解决原子性:synchronized和Lock实现
3.1.2 可见性:指当多个线程访问同一个变量时,一个线程修改了这个变量的值,其他线程能够立即看得到修改的值
解决可见性:volatile。synchronized和Lock也能够保证可见性
3.1.3 有序性:即程序执行的顺序按照代码的先后顺序执行
3.1.3.1 指令重排序:一般来说,处理器为了提高程序运行效率,可能会对输入代码进行优化,它不保证程序中各个语句的执行先后顺序同代码中的顺序一致,
但是它会保证程序最终执行结果和代码顺序执行的结果是一致的。重排序过程不会影响到单线程程序的执行,却会影响到多线程并发执行的正确性。
3.1.3.2 数据依赖性:如果一个指令Instruction 2必须用到Instruction 1的结果,那么处理器会保证Instruction 1会在Instruction 2之前执行
3.1.3.3 解决有序性:synchronized和Lock来保证有序性。volatile关键字来保证一定的“有序性”,volatile关键字能禁止指令重排序,所以volatile能在一定程度上保证有序性。
//x、y为非volatile变量.flag为volatile变量
//语句1、2一定发生再4、5之前,但不能保证语句1发生在2之前,4发生在5之前
x = 2; //语句1
y = 0; //语句2
flag = true; //语句3
x = 4; //语句4
y = -1; //语句5
3.1.3.4 Java内存模型具备一些先天的“有序性”,即不需要通过任何手段就能够得到保证的有序性,这个通常也称为 happens-before 原则。
如果两个操作的执行次序无法从happens-before原则推导出来,那么它们就不能保证它们的有序性,虚拟机可以随意地对它们进行重排序
happens-before原则(先行发生原则):
程序次序规则:一个线程内,按照代码顺序,书写在前面的操作先行发生于书写在后面的操作(不排除发生指令重排序可能)
锁定规则:一个unLock操作先行发生于后面对同一个锁额lock操作
volatile变量规则:对一个变量的写操作先行发生于后面对这个变量的读操作
传递规则:如果操作A先行发生于操作B,而操作B又先行发生于操作C,则可以得出操作A先行发生于操作C
线程启动规则:Thread对象的start()方法先行发生于此线程的每个一个动作
线程中断规则:对线程interrupt()方法的调用先行发生于被中断线程的代码检测到中断事件的发生
线程终结规则:线程中所有的操作都先行发生于线程的终止检测,我们可以通过Thread.join()方法结束、Thread.isAlive()的返回值手段检测到线程已经终止执行
对象终结规则:一个对象的初始化完成先行发生于他的finalize()方法的开始
四、synchronized
3.synchronized
3.1 注意事项:
1)当一个线程正在访问一个对象的synchronized方法,那么其他线程不能访问该对象的其他synchronized方法,其他线程能访问该对象的非synchronized方法
2)如果一个线程A需要访问对象object1的synchronized方法fun1,另外一个线程B需要访问对象object2的synchronized方法fun1,即使object1和object2是同一类型),
也不会产生线程安全问题,因为他们访问的是不同的对象
3)如果一个线程执行一个对象的非static synchronized方法,另外一个线程需要执行这个对象所属类的static synchronized方法,
此时不会发生互斥现象,因为访问static synchronized方法占用的是类锁,而访问非static synchronized方法占用的是对象锁
3.2 synchronized代码块比synchronized灵活,只对需要同步的地方进行同步
3.3 通过反编译指令javap -c 类名,可以知道synchronized代码块实际上多了一条monitorenter和两条monitorexit指令(一种就是执行完释放;另外一种就是发送异常)。
monitorenter指令执行时会让对象的锁计数加1,而monitorexit指令执行时会让对象的锁计数减1
五、lock
4.lock:Java 5之后,java.util.concurrent.locks包下提供一种接口,底层基于CAS(无锁算法,乐观锁)和ASQ(AbstractQueuedSynchronizer)实现
4.1 lock接口方法:
1)lock():锁已被其他线程获取,则进行等待;发生异常时,不会自动释放锁,所以必写在try-catch-finally进行,通过unlock()释放锁
2)tryLock():有返回值,可以知道是否成功获取锁
3)lockInterruptibly():如果线程正在等待获取锁,则中断线程的等待状态。声明抛出InterruptedException
4.2 ReentrantLock是唯一实现了Lock接口的类
4.3 ReentrantReadWriteLock:实现ReadWriteLock接口的类,readLock()和writeLock()用来获取读锁和写锁。相比synchronized可以多个线程同时进行读操作
4.3.1 注意事项
1)如果有一个线程已经占用了读锁,则此时其他线程如果要申请写锁,则申请写锁的线程会一直等待释放读锁。
2)如果有一个线程已经占用了写锁,则此时其他线程如果申请写锁或者读锁,则申请的线程会一直等待释放写锁
public
class
Test {
private
ReentrantReadWriteLock rwl =
new
ReentrantReadWriteLock();
public
static
void
main(String[] args) {
final
Test test =
new
Test();
new
Thread(){
public
void
run() {
test.get(Thread.currentThread());
};
}.start();
new
Thread(){
public
void
run() {
test.get(Thread.currentThread());
};
}.start();
}
public
void
get(Thread thread) {
rwl.readLock().lock();
try
{
long
start = System.currentTimeMillis();
while
(System.currentTimeMillis() - start <=
1
) {
System.out.println(thread.getName()+
"正在进行读操作"
);
}
System.out.println(thread.getName()+
"读操作完毕"
);
}
finally
{
rwl.readLock().unlock();
}
}
}
六、Lock和synchronized的区别
1)Lock是一个接口,而synchronized是Java中的关键字
2)synchronized在发生异常时,会自动释放线程占有的锁,因此不会导致死锁现象发生;
而Lock在发生异常时,如果没有主动通过unLock()去释放锁,则很可能造成死锁现象,因此使用Lock时需要在finally块中释放锁
3)Lock可以让等待锁的线程响应中断,而synchronized却不行,使用synchronized时,等待的线程会一直等待下去,不能够响应中断
4)通过Lock可以知道有没有成功获取锁,而synchronized却无法办到
5)Lock可以提高多个线程进行读操作的效率
七、wait、notify、notifyAll和Condition
6.1 wait():如果调用某个对象的wait()方法,当前线程必须拥有这个对象的锁,因此调用wait()方法必须在同步块或者同步方法中进行(synchronized块或者synchronized方法)
6.2 notify():能够唤醒一个正在等待该对象的monitor的线程,也是必须拥有这个对象的锁,在同步块或者同步方法中进行
6.3 nofityAll():能够唤醒所有正在等待该对象的monitor的线程。同notify()一样只是唤醒等待对象的monitor的线程,并不决定哪个线程能够立即获取到monitor,
只有等调用完notify()或者notifyAll()并退出synchronized块,释放对象锁后,其余线程才可获得锁执行
6.4 Condition:接口。更高效。,await()、signal()和signalAll()。lock.lock()和lock.unlock之间才可以使用。通过lock.newCondition()得到
案例:生产者-消费者模型的实现
1.使用Object的wait()和notify()实现:
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 22 23 24 25 26 27 28 29 30 31 32 33 34 35 36 37 38 39 40 41 42 43 44 45 46 47 48 49 50 51 52 53 54 55 56 57 58 59 |
|
2.使用Condition实现
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 22 23 24 25 26 27 28 29 30 31 32 33 34 35 36 37 38 39 40 41 42 43 44 45 46 47 48 49 50 51 52 53 54 55 56 57 58 59 60 61 62 63 64 65 66 67 68 69 70 71 72 73 74 |
|
八、Callable、Future和FutureTask
7.1 Callable与Runnable:java 1.5之后,java.util.concurrent包下,两者都是接口
7.1.1 Callable:只有一个call()方法,与ExecutorService结合使用
7.1.2 Future:对于具体的Runnable或者Callable任务的执行结果进行取消、查询是否完成、获取结果。必要时可以通过get方法获取执行结果,该方法会阻塞直到任务返回结果
public
interface
Future<V> {
boolean
cancel(
boolean
mayInterruptIfRunning);
boolean
isCancelled(); //是否已取消
boolean
isDone(); //是否已完成
V get()
throws
InterruptedException, ExecutionException;
V get(
long
timeout, TimeUnit unit)
throws
InterruptedException, ExecutionException, TimeoutException;
}
7.2 FutureTask:实现了RunnableFuture接口,后者继承了Runnable接口和Future接口,是future接口唯一实现类
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 22 23 24 25 26 27 28 29 30 31 32 33 34 35 36 37 |
|
九、ConcurrentHashMap
8.ConcurrentHashMap:相比Hashtable(读写时都去竞争一把锁)效率高,读取数据不加锁,写操作粒度尽量减小
7.1 内容结构:内部采用Segment结构,一个Segment其实就是一个类Hash Table的结构,Segment内部维护了一个链表数组
ConcurrentHashMap定位一个元素的过程需要进行两次Hash操作,第一次Hash定位到Segment,第二次Hash定位到元素所在的链表的头部,
因此,这一种结构的带来的副作用是Hash的过程要比普通的HashMap要长,但是带来的好处是写操作的时候可以只对元素所在的Segment进行加锁即可,
不会影响到其他的Segment,大大的提高并发能力
十、 阻塞队列
9.非阻塞队列:priorityQueue、LinkedList(LinkedList是双向链表,它实现了Dequeue接口)
阻塞队列:Java 1.5之后,java.util.concurrent包下提供了如下队列
ArrayBlockingQueue:基于数组、先进先出、容量有界。创建ArrayBlockingQueue对象时必须制定容量大小。并且可以指定公平性与非公平性,默认情况下为非公平的
LinkedBlockingQueue:基于链表、先进先出、容量有界。创建LinkedBlockingQueue对象时如果不指定容量大小,则默认大小为Integer.MAX_VALUE
PriorityBlockingQueue:优先级进出。容量*
DelayQueue:基于PriorityQueue,延时阻塞队列。只有当其指定的延迟时间到了,才能够从队列中获取到元素。容量*
详情访问https://blog.csdn.net/qq_33314107/article/details/80587082
十一、Timer
1.timer:线程安全
Timer timer = new Timer();
timer.schedule(new TimerTask() {
public void run() {
System.out.println("abc");
}
}, 200000 , 1000);
Timer和TimerTask的简单组合是多线程的嘛?不是,
一个Timer内部包装了“一个Thread”和“一个Task”队列,这个队列按照一定的方式将任务排队处理,包含的线程在Timer的构造方法调用时被启动,
这个Thread的run方法无限循环这个Task队列,若队列为空且没发生cancel操作,此时会一直等待,如果等待完成后,队列还是为空,
则认为发生了cancel从而跳出死循环,结束任务;循环中如果发现任务需要执行的时间小于系统时间,则需要执行,
那么根据任务的时间片从新计算下次执行时间,若时间片为0代表只执行一次,则直接移除队列即可。
但是是否能实现多线程呢?可以,任何东西是否是多线程完全看个人意愿,多个Timer自然就是多线程的,每个Timer都有自己的线程处理逻辑,
当然Timer从这里来看并不是很适合很多任务在短时间内的快速调度,至少不是很适合同一个timer上挂很多任务,在多线程的领域中我们更多是使用多线程中的:Executors.newScheduledThreadPool
十二、ThreadLocal
12.1 提供的方法
public
T get() { } //设置当前线程中变量的副本
public
void
set(T value) { } //获取当前线程中保存的变量副本
public
void
remove() { } //移除当前线程中变量的副本
protected
T initialValue() { } //用来在使用时进行重写的,它是一个延迟加载方法
12.2 最常见的ThreadLocal使用场景为 用来解决 数据库连接、Session管理等
1)数据库连接
private
static
ThreadLocal<Connection> connectionHolder
=
new
ThreadLocal<Connection>() {
public
Connection initialValue() {
return
DriverManager.getConnection(DB_URL);
}
};
public
static
Connection getConnection() {
return
connectionHolder.get();
}
2)session管理
private
static
final
ThreadLocal threadSession =
new
ThreadLocal();
public
static
Session getSession()
throws
InfrastructureException {
Session s = (Session) threadSession.get();
try
{
if
(s ==
null
) {
s = getSessionFactory().openSession();
threadSession.set(s);
}
}
catch
(HibernateException ex) {
throw
new
InfrastructureException(ex);
}
return
s;
}
十三、ExecutorService线程池
13.1 ExecutorService:接口,java.util.concurrent包下。继承了Executor
接口,该接口只有execute()方法
13.2 Executors提供四种线程池,分别为:
newCachedThreadPool()创建一个可缓存线程池,如果线程池长度超过处理需要,可灵活回收空闲线程,若无可回收,则新建线程。
newFixedThreadPool(3) 创建一个定长线程池,可控制线程最大并发数,超出的线程会在队列中等待。
newScheduledThreadPool 创建一个定长线程池,支持定时及周期性任务执行。
newSingleThreadExecutor() 创建一个单线程化的线程池,它只会用唯一的工作线程来执行任务,保证所有任务按照指定顺序(FIFO, LIFO, 优先级)执行。
ScheduledExecutorService scheduledThreadPool = Executors.newScheduledThreadPool(5);
scheduledThreadPool.scheduleAtFixedRate(new Runnable() {
@Override
public void run() {
System.out.println("delay 1 seconds, and excute every 3 seconds");
}
}, 1, 3, TimeUnit.SECONDS); //表示延迟1秒后每3秒执行一次。
13.3 ExecutorService 的submit()与execute()区别:
1)接收的参数不一样 submit()可以接受runnable无返回值和callable有返回值 execute()接受runnable 无返回值
2)submit方便Exception处理,通过捕获Future.get抛出的异常
13.4 ExecutorService 的shotdown()与showdownNow()区别
1)shutdown() 方法在终止前允许执行以前提交的任务
2)showdownNow()阻止等待任务启动并试图停止当前正在执行的任务。在终止时执行程序没有任务在执行,也没有任务在等待执行,并且无法提交新任务