基础
进程和线程
进程就是一个正在执行性的应用程序,比如微信,哔哩哔哩等
线程就是一个进程中的执行单元,一个进程中能产生多个线程.但是同一个进程中的这些线程共享进程中的堆和方法区资源,但每个线程自己的栈和程序计数器不共享,所以进程之间切换工作负担要比进程小。
堆和方法区共享:堆是进程中的一块内存,存放创建的对象,方法区存放被加载的类信息,常量,静态变量等
程序计数器私有:程序计数器是用来记录当前执行到哪里,以及之后要执行哪里.如果程序计数器共享而被其他线程修改,会让当前线程无法准确执行下去
栈私有:为了保证线程局部变量不被其他线程访问
并发与并行
并发:针对单核CPU提出的,是两个或两个以上的执行单元在同一时间段内执行(图源:http://c.biancheng.net/view/9486.html)
并行
判断并发还是并行在于是否同时进行
如今一个系统上要同时跑很多进程,只有4核,8核进行并行是不够的,是需要同时并发加并行
如今我们的计算机都是多核,如果我们要处理一个复杂的任务,只使用一个线程来处理就会造成只有一个CPU核心被使用.而是用多线程,可以使这些线程被映射到多个CPU上,任务执行效率会显著提高.
虽然并发编程能够提高任务执行效率,但并发可能会遇到如:内存泄漏,死锁,线程不安全等
线程生命周期
图源("https://www.bilibili.com/video/BV1mE411x7Wt?
线程安全
场景
我和小鹏一起去银行账户取钱,当我到银行取钱时取了10000元,本来应该在银行账户减去10000元,但是此时因为网络延迟而是账户的余额没来得及减去10000,由于是多线程编程,不用等此方法执行完,小鹏又去取钱,有多分出来一个线程且两个线程共享银行账户此时银行账户还是10000元,可以取到10000元。等到取钱完毕银行账户刷新两次为0元,此时银行就平白失去了10000元
public class ThreadTest {
@Test
public void test01() throws IOException {
Ticket ticket=new Ticket();
new Thread(()->{
System.out.println(ticket.get_ticket());
},"t1").start();
new Thread(()->{
System.out.println(ticket.get_ticket());
},"t2").start();
}
}
class Ticket{
private int money=100;
public int get_ticket(){
if(money==100){
try {
Thread.sleep(2);
} catch (InterruptedException e) {
e.printStackTrace();
}
money=0;
return 100;
}else{
return 0;
}
}
}
结果为:
100
100
什么时候会有并发安全问题
三个条件:1.多线程并发
2.有共享数据
3.共享数据有修改行为
解决线程安全问题
解决线程安全问题就是避免共享数据在并发时候被修改
怎么解决线程安全问题:线程排队执行(不能并发)
这种机制被称为:线程同步机制(线程同步)
虽然线程排队会牺牲一部分效率,但是数据安全最重要
锁
同步和异步
- 异步编程:
线程1和线程2,各自执行各自的,t1不管t2,t2不管t1,谁也不需要等谁,这种叫做异步编程模型.(效率高) - 同步编程:
线程1和线程2,在线程1执行的时候,必须等待2线程执行结束,或者在t2线程执行的时候,必须等待t2线程执行结束(效率低 )
synchronized
synchronized(){
//线程同步代码块
}
synchronized后面小括号写的数据是多线程共享的数据
该数据怎么写,需要看想让哪些线程同步
比如有t1,t2,t3,t4四个线程,如果只希望t1,t2排队而t3,t4不需要排队,那么要在括号内写t1,t2共享的数据,而此数据对t3,t4不共享
对上面线程不安全代码改写
public class ThreadTest {
@Test
public void test01() throws IOException {
Ticket ticket=new Ticket();
new Thread(()->{
System.out.println(ticket.get_ticket());
},"t1").start();
new Thread(()->{
System.out.println(ticket.get_ticket());
},"t2").start();
}
}
class Ticket{
private int money=100;
public int get_ticket(){
synchronized (this){
if(money==100){
try {
Thread.sleep(1);
} catch (InterruptedException e) {
e.printStackTrace();
}
money=0;
return 100;
}else{
return 0;
}
}
}
}
结果是
0
100
或者
100
0
他们都是同一个Ticket对象,this(不一定是this,其他的也可以只要是线程共享的)是他们共享的,所以可以保证线程安全
会形成这样结果的原因是:假如t1先执行这个方法,t1就会得到然后占有这把锁,执行同步代码块中的程序,并且是一直占有这把锁,直到同步代码块结束,才释放这把锁.当t1占有这把锁的时候t2来执行这个方法,也会遇到synchronized,就去占有这把锁,但是现在锁被t1占有,t2只能等t1执行完释放锁的时候得到这把锁
这样就达到了同步机制
将synchronized写在实例方法上一定锁的是this,不能是其他对象,这种方式不灵活,也可能无故扩大同步范围,效率低不常用
synchronized写在静态方法上用的是类锁
但当使用的锁是this且范围是整个方法,建议用在实例方法上
public synchronized int get_ticket(){
if(money==100){
try {
Thread.sleep(1);
} catch (InterruptedException e) {
e.printStackTrace();
}
money=0;
return 100;
}else{
return 0;
}
}
不同种类的变量线程安全问题
- 实例变量在堆区,堆只有一个,存在线程安全问题
- 静态变量在方法区,方法区只有一个,存在线程安全问题
- 成员变量在栈中,一个线程私有自己的栈,不存在线程安全问题
比如当选择使用StringBuilder(线程不安全)还是StringBuffer(线程安全)时,如果使用局部变量的话建议使用StringBuilder因为局部变量不存在线程安全问题这样效率更高,因为如果使用StringBuilder即使是局部变量也要每次去锁池
死锁
线程死锁描述的是这样一种情况:多个线程同时被阻塞,它们中的一个或者全部都在等待某个资源被释放。由于线程被无限期地阻塞,因此程序不可能正常终止。
代码
public static void main(String[] args) {
new Thread(() -> {
synchronized (resource1) {
System.out.println(Thread.currentThread() + "get resource1");
try {
Thread.sleep(1000);
} catch (InterruptedException e) {
e.printStackTrace();
}
System.out.println(Thread.currentThread() + "waiting get resource2");
synchronized (resource2) {
System.out.println(Thread.currentThread() + "get resource2");
}
}
}, "t1").start();
new Thread(() -> {
synchronized (resource2) {
System.out.println(Thread.currentThread() + "get resource2");
try {
Thread.sleep(1000);
} catch (InterruptedException e) {
e.printStackTrace();
}
System.out.println(Thread.currentThread() + "waiting get resource1");
synchronized (resource1) {
System.out.println(Thread.currentThread() + "get resource1");
}
}
}, "t2").start();
线程 A 通过 synchronized (resource1) 获得 resource1 的监视器锁,然后通过Thread.sleep(1000);让线程 A 休眠 1s 为的是让线程 B 得到执行然后获取到 resource2 的监视器锁。线程 A 和线程 B 休眠结束了都开始企图请求获取对方的资源,然后这两个线程就会陷入互相等待的状态,这也就产生了死锁。
上面的例子符合产生死锁的四个必要条件:
互斥条件:该资源任意一个时刻只由一个线程占用。
请求与保持条件:一个线程因请求资源而阻塞时,对已获得的资源保持不放。
不剥夺条件:线程已获得的资源在未使用完之前不能被其他线程强行剥夺,只有自己使用完毕后才释放资源。
循环等待条件:若干线程之间形成一种头尾相接的循环等待资源关系。
参考:bilibili动力节点老杜