Java多线程基础二

线程不安全的原因

  1. 线程之间是抢占式执行的【根本原因】
  2. 多个线程修改同一个变量
    为了规则这类线程安全问题,可以尝试变换代码的组织形式,达到一个线程只改一个变量
  3. 原子性指一个或多个操作,要么全部执行且在执行过程中不被任何因素打断,要么全部不执行。
  4. 内存可见性指, 一个线程对共享变量值的修改,能够及时地被其他线程看到(和编译器优化相关)
  5. 指令重排序(和编译器优化相关)

如何解决线程不安全的问题

以原子性为切入点,使用synchronized关键字

synchronized加锁

先来看一个两个线程同时改变一个变量的例子

	static  class Count{
        public int count=0;
        public void increase(){
            count++;
        }
    }
    public static void main(String[] args) {
        Count count = new Count();
        Thread t1 = new Thread(new Runnable() {
            @Override
            public void run() {
                for (int i = 0; i < 50000; i++) {
                    count.increase();
                }
            }
        });
        Thread t2 = new Thread(new Runnable() {
            @Override
            public void run() {
                for (int i = 0; i < 50000; i++) {
                    count.increase();
                }
            }
        });

        t1.start();
        t2.start();

        try {
            t1.join();
            t2.join();
        } catch (InterruptedException e) {
            e.printStackTrace();
        }
        System.out.println(count.count);
    }

运行之后,我们发现得到的结果并不是我们所期望的100000;所以我们通过synchronized关键字来进行加锁操作。

以下是加锁的两种方式,都能够保证线程安全。

synchronized如果是修饰代码块的时候,需要显示的在()中指定一个要加锁的对象。

  • 如果是synchronized直接修饰的非静态方法,相当于加锁的对象就是this。
  • 修饰代码块中加锁对象是类对象的话
		public void increase(){
            synchronized (this){
                count++;
            }
        }

synchronized可以直接用于修饰普通方法或者静态方法。修饰静态方式的时候,相当于针对类对象进行加锁。【由于类对象是单例的,两个线程并发调用该方法,一定会触发锁竞争】

		synchronized public void increase(){
                count++;
        }

锁的由来:能够修饰一个方法/代码块。进入代码块,就加锁,出了代码块就解锁。

synchronized保证了代码块在任意时刻最多只有一个线程能执行(互斥性)。
synchronized的功能本质就是把“并发”变成“串行”

synchronized需要指定一个具体的加锁对象。
如果是修饰一个非静态方法,加锁对象就是this.
如果是修饰一个静态方法,加锁对象就是当前的类的对象。
如果修饰一个代码块,加锁对象通过()来指定。

两个线程竞争同一把锁,才可能出现阻塞。

当线程1获取到锁之后,线程2也尝试获取锁,就会出现阻塞等待的请款。直到线程1释放了锁,线程2才有可能获取到锁。

synchronized还能保证内存可见性。
在synchronized内部如果要访问变量,就会保证一定能操作内存(禁止了优化,例如保证每一次CPU都从内存中读取最新的数据,修改之后,每一次都将数据写回到内存,不允许省略操作)

synchronized实现了可重入性

		synchronized public void increase(){
            synchronized (this){
                count++;
            }
        }

synchronized内部记录当前这个锁是哪个线程持有的。

Java标准库(集合类)中的线程安全类
Java集合类中很多都是线程不安全的,这些类可能会涉及到多线程修改共享数据,又没有任何加锁措施。

  • ArrayList
  • LinkedList
  • HashMap
  • TreeMap
  • HashSet
  • TreeSet
  • StringBuilder

有一些是线程安全的,使用了一些锁机制来控制

  • Vertor
  • HashTable
  • ConcurrentHashMap
  • StringBuffer

还有虽然没有加锁,但是不涉及“修改”,仍然是线程安全的。

  • String

以内存可见性为切入点,使用volatile关键字,但是不能保证原子性(与synchronized的不同)

因此,volatile关键字的目的是告诉虚拟机:

  • 每次访问变量时,总是获取主内存的最新值;
  • 每次修改变量后,立刻回写到主内存。

先来看一个例子

public static int count = 0;
    
    public static void main(String[] args) {

        Thread t1 = new Thread(new Runnable() {
            @Override
            public void run() {
                while (count==0){

                }
                System.out.println("循环结束");
            }
        });
        t1.start();
        Scanner sc = new Scanner(System.in);
        System.out.println("请输入一个整数");
        count = sc.nextInt();
    }

我们输入1之后,线程并没有结束。错误的原因是快速的循环,频繁的从内存中读取数据,编译器进行了优化->并不是每一次都从内存(主内存)中读取数据,而是从工作内存中读取。

加入volatile之后,禁止了编译器优化,保证每次读取的数据都是从主内存中读取。

volatile public static int count = 0;
  • 1
    点赞
  • 0
    收藏
    觉得还不错? 一键收藏
  • 0
    评论
评论
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

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

抵扣说明:

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

余额充值