Java多线程一


基础

进程和线程

进程就是一个正在执行性的应用程序,比如微信,哔哩哔哩等
线程就是一个进程中的执行单元,一个进程中能产生多个线程.但是同一个进程中的这些线程共享进程中的堆和方法区资源,但每个线程自己的栈和程序计数器不共享,所以进程之间切换工作负担要比进程小。
在这里插入图片描述

堆和方法区共享:堆是进程中的一块内存,存放创建的对象,方法区存放被加载的类信息,常量,静态变量等

程序计数器私有:程序计数器是用来记录当前执行到哪里,以及之后要执行哪里.如果程序计数器共享而被其他线程修改,会让当前线程无法准确执行下去

栈私有:为了保证线程局部变量不被其他线程访问

并发与并行

并发:针对单核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动力节点老杜

  • 2
    点赞
  • 0
    收藏
    觉得还不错? 一键收藏
  • 1
    评论

“相关推荐”对你有帮助么?

  • 非常没帮助
  • 没帮助
  • 一般
  • 有帮助
  • 非常有帮助
提交
评论 1
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

当前余额3.43前往充值 >
需支付:10.00
成就一亿技术人!
领取后你会自动成为博主和红包主的粉丝 规则
hope_wisdom
发出的红包
实付
使用余额支付
点击重新获取
扫码支付
钱包余额 0

抵扣说明:

1.余额是钱包充值的虚拟货币,按照1:1的比例进行支付金额的抵扣。
2.余额无法直接购买下载,可以购买VIP、付费专栏及课程。

余额充值