一.线程的相关概念:
1. 并行和并发
并行:
同一时刻,可以同时处理事情的能力。
并行是指多个处理器或者是多核的处理器同时处理多个不同的任务。
并发:
与单位时间相关,在单位时间内可以处理事情的能力。
并发是指一个处理器同时处理多个任务。
例如:并发是一个人同时吃三个馒头,而并行是三个人同时吃三个馒头
2. 进程和线程
进程:
程序运行资源分配的最小单位,进程内部有多个线程,会共享这个进程的资源
线程:
CPU调度的最小单位,必须依赖进程而存在。
进程和线程的区别:
- 从属关系:进程是正在运行程序的实例,进程包含了线程。
- 描述侧重点:进程是操作系统分配资源的基本单位,线程是操作系统调度的基本单位。
- 共享资源:多个进程不能共享资源,每个进程有自己的堆、栈、虚拟空间、文件描述等,而线程可以共享堆、方法。
- 上下文切换速度:进程比较快(从一个线程切换到另一个线程),线程会比较慢。
- 操作者:一般来说进程由系统控制,线程由程序员控制。
3.线程的优先级
取值为1~10,缺省为5,但线程的优先级不可靠,不建议作为线程开发时候的手段、
4. 守护线程 & 用户线程
- User Thread(用户线程)
普通线程,例如普通当new Thread
- Daemon Thread(守护线程)
thread1.setDaemon(true); 也即设置了 setDaemon的线程
当 JVM 中不存在任何一个正在运行的非守护线程时,则 JVM 进程即会退出, 大白话就是当主线程结束时,此时普通当用户线程会继续执行,
当所有当用户线程执行完后,即结束所有线程,此时如果还有守护线程在执行,也会立即结束。
5.并发出现问题的根源: 并发三要素
可见性是由于CPU缓存引起。
可见性:一个线程对共享变量的修改,另外一个线程能够立刻看到。如:
//线程1执行的代码
int i = 0;
i = 10;
//线程2执行的代码
j = i;
假若执行线程1的是CPU1,执行线程2的是CPU2。由上面的分析可知,当线程1执行 i =10这句时,会先把i的初始值加载到CPU1的高速缓存中,然后赋值为10,那么在CPU1的高速缓存当中i的值变为10了,却没有立即写入到主存当中。
此时线程2执行 j = i,它会先去主存读取i的值并加载到CPU2的缓存当中,注意此时内存当中i的值还是0,那么就会使得j的值为0,而不是10.
原子性问题是分时复用引起。
原子性:即一个操作或者多个操作 要么全部执行并且执行的过程不会被任何因素打断,要么就都不执行。
如:
int i = 1;
// 线程1执行
i += 1;
// 线程2执行
i += 1;
1. 将变量 i 从内存读取到 CPU寄存器;
2. 在CPU寄存器中执行 i + 1 操作;
3. 将最后的结果i写入内存(缓存机制导致可能写入的是 CPU 缓存而不是内存)。
由于CPU分时复用(线程切换)的存在,线程1执行了第一条指令后,就切换到线程2执行,假如线程2执行了这三条指令后,再切换会线程1执行后续两条指令,将造成最后写到内存中的i值是2而不是3。
有序性问题是重排序引起。
int i = 0;
boolean flag = false;
i = 1; //语句1
flag = true; //语句2
二.线程的生命周期
线程的初始化到终止的整个过程,称为线程的生命周期
新生状态
当线程对象被实例化后,就进入了新生状态。new Thread()
就绪状态
当某个线程对象调用了start()方法后,就进入了就绪状态。
在该状态下,线程对象不会做任何事情,只是在等待CPU调用。
运行状态
当某个线程对象得到运行的机会后,则进入运行状态,开始执行run()方法。
不会等待run()方法执行完毕,只要run()方法调用后,该线程就会再进入就绪状态。这时其他线程就有可能穿插其中。
阻塞状态
如果某个线程遇到了sleep()方法或wait()等方法时,就会进入阻塞状态。
sleep()方法会在一段时间后自动让线程重新就绪。
wait()方法只有在被调用notify()或notifyAll()方法唤醒后才能进入就绪状态。
终止状态
当某个线程的run()方法中所有内容都执行完,就会进入终止状态,意味着该线程的使命已经完成。
三.线程的常用方法:
四.线程同步
为何要使用同步?
- 可重入锁:在执行对象中所有同步方法不用再次获得锁
- 可中断锁:在等待获取锁过程中可中断
- 公平锁: 按等待获取锁的线程的等待时间进行获取,等待时间长的具有优先获取锁权利
- 读写锁:对资源读取和写入的时候拆分为2部分处理,读的时候可以多线程一起读,写的时候必须同步地写
常用同步方法比较:
ReentrantLock ReentrantLock是Lock接口的实现
ReentrantLock是JDK方法,需要手动声明上锁和释放锁,因此语法相对复杂些;如果忘记释放锁容易导致死锁
锁的获取: 分情况而定,Lock有多个锁获取的方式,大致就是可以尝试获得锁,线程可以不用一直等待
锁的状态:可以判断
锁的类型:可重入 可中断 可公平(两者皆可)
性能 Lock: 大量同步 Lock可以提高多个线程进行读操作的效率。(可以通过readwritelock实现读写分离)
在资源竞争不是很激烈的情况下,Synchronized的性能要优于ReetrantLock,但是在资源竞争很激烈的情况下,Synchronized的性能会下降几十倍,但是ReetrantLock的性能能维持常态;
ReentrantLock提供了多样化的同步,比如有时间限制的同步,可以被Interrupt的同步(synchronized的同步是不能Interrupt的)等。在资源竞争不激烈的情形下,性能稍微比synchronized差点点。但是当同步非常激烈的时候,synchronized的性能一下子能下降好几十倍。而ReentrantLock确还能维持常态。
6. ReentrantLock具有更好的细粒度,可以在ReentrantLock里面设置内部Condititon类,可以实现分组唤醒需要唤醒的线程
Synchronized
1.Synchoronized是JVM方法,由编辑器保证枷锁和释放,在发生异常时候会自动释放占有的锁,因此不会出现死锁
2.锁的获取:假设A线程获得锁,B线程等待。如果A线程阻塞,B线程会一直等待
3.锁的状态:无法判断
4.锁的类型: 可重入 不可中断 非公平
一、两种锁的底层实现方式:
synchronized:底层使用指令码方式来控制锁的,映射成字节码指令就是增加来两个指令:monitorenter和monitorexit。当线程执行遇到monitorenter指令时会尝试获取内置锁,如果获取锁则锁计数器+1,如果没有获取锁则阻塞;当遇到monitorexit指令时锁计数器-1,如果计数器为0则释放锁。
Look底层主要靠volatile和CAS操作实现的,底层是CAS乐观锁,依赖AbstractQueuedSynchronizer类,把所有的请求线程构成一个CLH队列。而对该队列的操作均通过Lock-Free(CAS)操作
二、在jdk1.6~jdk1.7的时候,synchronized优化:
1、线程自旋和适应性自旋
我们知道,java’线程其实是映射在内核之上的,线程的挂起和恢复会极大的影响开销。并且jdk官方人员发现,很多线程在等待锁的时候,在很短的一段时间就获得了锁,所以它们在线程等待的时候,并不需要把线程挂起,而是让他无目的的循环,一般设置10次。这样就避免了线程切换的开销,极大的提升了性能。
而适应性自旋,是赋予了自旋一种学习能力,它并不固定自旋10次一下。他可以根据它前面线程的自旋情况,从而调整它的自旋,甚至是不经过自旋而直接挂起。
2、锁消除:把不必要的同步在编译阶段进行移除。
3、锁粗化
4、轻量级锁
5、偏向锁
1.同步方法
即有synchronized关键字修饰的方法。
由于java的每个对象都有一个内置锁,当用此关键字修饰方法时, 内置锁会保护整个方法。在调用该方法前,需要获得内置锁,否则就处于阻塞状态。
代码:
public synchronized void save(){}
注: synchronized关键字也可以修饰静态方法,此时如果调用该静态方法,将会锁住整个类
2.同步代码块
即有synchronized关键字修饰的语句块。
被该关键字修饰的语句块会自动被加上内置锁,从而实现同步
代码:
synchronized(object){
}
注:同步是一种高开销的操作,因此应该尽量减少同步的内容。
通常没有必要同步整个方法,使用synchronized代码块同步关键代码即可。
代码实例:
package com.xhj.thread;
/**
* 线程同步的运用
*
* @author XIEHEJUN
*
*/
public class SynchronizedThread {
class Bank {
private int account = 100;
public int getAccount() {
return account;
}
/**
* 用同步方法实现
*
* @param money
*/
public synchronized void save(int money) {
account += money;
}
/**
* 用同步代码块实现
*
* @param money
*/
public void save1(int money) {
synchronized (this) {
account += money;
}
}
}
class NewThread implements Runnable {
private Bank bank;
public NewThread(Bank bank) {
this.bank = bank;
}
@Override
public void run() {
for (int i = 0; i < 10; i++) {
// bank.save1(10);
bank.save(10);
System.out.println(i + "账户余额为:" + bank.getAccount());
}
}
}
/**
* 建立线程,调用内部类
*/
public void useThread() {
Bank bank = new Bank();
NewThread new_thread = new NewThread(bank);
System.out.println("线程1");
Thread thread1 = new Thread(new_thread);
thread1.start();
System.out.println("线程2");
Thread thread2 = new Thread(new_thread);
thread2.start();
}
public static void main(String[] args) {
SynchronizedThread st = new SynchronizedThread();
st.useThread();
}
}
3.使用特殊域变量(volatile)实现线程同步
代码示例:
//只给出要修改的代码,其余代码与上同
class Bank {
//需要同步的变量加上volatile
private volatile int account = 100;
public int getAccount() {
return account;
}
//这里不再需要synchronized
public void save(int money) {
account += money;
}
}
注:多线程中的非同步问题主要出现在对域的读写上,如果让域自身避免这个问题,则就不需要修改操作该域的方法。
用final域,有锁保护的域和volatile域可以避免非同步的问题。
4.使用重入锁实现线程同步
例如: 在上面例子的基础上,改写后的代码为:
代码实例:
注:关于Lock对象和synchronized关键字的选择:
a.最好两个都不用,使用一种java.util.concurrent包提供的机制,
能够帮助用户处理所有与锁相关的代码。
b.如果synchronized关键字能满足用户的需求,就用synchronized,因为它能简化代码
c.如果需要更高级的功能,就用ReentrantLock类,此时要注意及时释放锁,否则会出现死锁,通常在finally代码释放锁
5.使用局部变量实现线程同步
如果使用ThreadLocal管理变量,则每一个使用该变量的线程都获得该变量的副本,
副本之间相互独立,这样每一个线程都可以随意修改自己的变量副本,而不会对其他线程产生影响。
例如: 在上面例子基础上,修改后的代码为:
6.使用阻塞队列实现线程同步
LinkedBlockingQueue 类常用方法
LinkedBlockingQueue() : 创建一个容量为Integer.MAX_VALUE的LinkedBlockingQueue
put(E e) : 在队尾添加一个元素,如果队列满则阻塞
size() : 返回队列中的元素个数
take() : 移除并返回队头元素,如果队列空则阻塞
7.使用原子变量实现线程同步
需要使用线程同步的根本原因在于对普通变量的操作不是原子的。
那么什么是原子操作呢?
原子操作就是指将读取变量值、修改变量值、保存变量值看成一个整体来操作
即-这几种行为要么同时完成,要么都不完成。
在java的util.concurrent.atomic包中提供了创建了原子类型变量的工具类,
使用该类可以简化线程同步。
其中AtomicInteger 表可以用原子方式更新int的值,可用在应用程序中(如以原子方式增加的计数器),
但不能用于替换Integer;可扩展Number,允许那些处理机遇数字类的工具和实用工具进行统一访问。
AtomicInteger类常用方法:
AtomicInteger(int initialValue) : 创建具有给定初始值的新的AtomicInteger
addAddGet(int dalta) : 以原子方式将给定值与当前值相加
get() : 获取当前值
多线程相关面试题
-
实现多线程的方式?
-
继承Thread类
-
实现Runnable接口后,包装为Thread对象
-
使用匿名内部类
-
-
为什么说StringBuilder是非线程
-
什么叫死锁?怎么产生?如何解决?
如有两个人吃西餐,必须要有刀和叉才能吃饭,只有一副刀叉。
如果A拿到了刀,B拿到了叉,互相都在等待另一个工具,但都不释放自己的,
这时就会造成死锁的局面,既不结束,也不继续。
模拟死锁出现的原因:
定义两个线程类,线程A先获取资源A后,再获取资源B;线程B先获取资源B后,再获取资源A。
如果对A和B对使用了synchronized进行同步,就会在线程A获取资源A时候,线程B无法获取资源A,
相反线程B在获取资源B的时候,线程A无法获取资源B,所以两个线程都不会得到另一个资源。
死锁的解决方式一:
让线程A和线程B获取资源A和资源B的顺序保持一致。
如让两个线程都先获取fork,再获取knife
死锁的解决方式二:
让线程A和线程B获取资源A和资源B之前,再获取第三个资源,并对其使用synchronized进行同步,
这样线程A在获取第三个资源后,将所有事情执行完后,线程B才能继续。
如在获取fork和knife之前,先获取paper对象