三、线程并发基础之Thread安全

1. Java内存模型与多线程

数据读取顺序优先级:寄存器->高速缓存->内存。
统一内存管理模型:JMM(Java Memory Model),屏蔽底层平台内存管理细节,在多线程环境中必须解决可见性和有序性。
主内存:Java堆内存,存放所有类实例、静态数据等变量,多个线程共享。
工作内存:该线程从主内存拷贝过来的变量以及访问方法所获得的局部变量,线程私有。
请添加图片描述
可见性问题:多个线程之间不能通信,只能通过共享变量,每new一个对象会被分配到主内存中,每个线程会在工作内存中存储自己的对象副本,工作内存大小有限制。执行顺序:
从主存复制变量到当前工作内存;
执行代码,改变共享变量值;
用工作内存数据刷新主存相关内容。
时序性问题:线程无法直接从主内存中引用变量,需要从主内存中拷贝一个副本(read-load)完成后引用该副本。同一线程再次引用该字段时,可能重新从主存中获取副本,也可能引用原来的(use),read、load、use顺序可以由JVM决定。

2. 什么是线程不安全

2.1 多个线程同时操作一个数据结构的时候产生相互修改和串行的情况,没有保证数据一致性。

package 线程安全问题;

/**
 * @Author KanLina
 * @Description 单个用户干活类
 * @Date 10/29/21 10:58 AM
 **/
public class Count {
    public int num = 0;

    public void add() {
        try {
            Thread.sleep(51);
        } catch (InterruptedException e) {
            e.printStackTrace();
        }
        num += 1;
        System.out.println(Thread.currentThread().getName() + "-" + num);
    }
}
package 线程安全问题;

/**
 * @Author KanLina
 * @Description 用户类,干Count的活
 * @Date 10/29/21 11:00 AM
 **/
public class ThreadA extends Thread {

    private Count count;
    public ThreadA (Count count){
        this.count = count;
    }
    public void run(){
        count.add();
    }
}
package 线程安全问题;

/**
 * @Author KanLina
 * @Description 5个人干活
 * @Date 10/29/21 11:01 AM
 **/
public class ThreadMain {
    public static void main(String[] args) {
        Count count = new Count();
        for (int i = 0; i < 5; i++) {
            ThreadA threadA = new ThreadA(count);
            threadA.start();
        }

        try{
            Thread.sleep(1001);
        } catch (InterruptedException e) {
            e.printStackTrace();
        }
        System.out.println("5个人干完活:最后的值是"+count.num);
    }
}

运行结果: 请添加图片描述

由于线程不安全,每次运行结果都不一致。

3. 什么是线程安全

保证数据的高度一致性和准确性。
将上次的count类加上 synchronized 请添加图片描述
请添加图片描述
实现线程安全:
多实例,不实用单例模式;
使用java.util.concurrent下的类库;
使用锁机制 synchronized、lock方式。

4. synchronized隐式锁,线程同步

4.1 修饰方法或者代码块保证同一时刻最多只有一个线程执行。
4.2 使用:方法声明时,放在范围操作符(public等)之后,返回类型声明(void等)之前;修饰在代码块上,指定加锁对象, synchronized(Object)。
4.3 隐式规则:
两个并发线程同时访问对象中 synchronized同步代码块时,一个时间内只能有一个线程得到执行。
一个线程访问对象中 synchronized同步代码块时,另一个线程仍然可以访问非同步代码块;
一个线程访问 对象中synchronized时,其他线程对该对象中的其他同步代码块访问被阻塞;
性能和执行效率的优劣程度从差到优排序:
同步方法体 请添加图片描述
小于
同步代码块 请添加图片描述
小于请添加图片描述
最后一种在工作中很常见,锁是对象,加锁和释放锁都需要此对象的资源,对象肯定越小越好,所以造一个字节的byte对象最小。

5. Lock显示锁和ReentrantLock可重入锁

5.1 Lock:无条件、可轮询、定时、可中断的锁获取操作。加锁和解锁的方法都是显示的。
包:java.util.concurrent.locks.Lock。核心方法:lock(),unlock(),tryLock(),
实现类:ReentrantLock\ReentrantReadWriteLock.ReadLock,ReentrantReadWriteLock.WriteLock。
Lock接口有如下方法:
void lock(); 获取锁,锁不可用将禁用当前线程,获取锁之前线程一直处于休眠状态。
void lockInterruptibly() throws InterruptedException;当前线程被中断则获取锁。线程处于休眠状态:锁由当前线程获得;或者其他某个线程中断当前线程,并且支持对锁获取的中断。如果当前线程:在进入此方法时已经设置了该线程的中断状态;或者在获取锁时被中断,并且支持对锁获取的中断,则将抛出异常,并清除当前线程的已中断状态。
boolean tryLock();调用时锁为空闲状态才获取锁。根据锁是否可有返回true或者false
boolean tryLock(long time, TimeUnit unit) throws InterruptedException;如果锁在给定的等待时间内空闲则获取锁并且当前线程未被中断。锁不可用并发生以下三种情况之一则线程处于休眠状态:void unlock();释放锁;new Condition();返回用来与此lock实例一起使用的Condition实例。
void unlock();
5.2 ReentrantLock; 可重入锁 ;是Lock的实现类。比synchronized调度时间少,但是可能忘记释放锁定。方法之间可以使用同一个锁,也可以使用不同锁。
使用:

public class ReentrantLockUse {
    private final ReentrantLock lock = new ReentrantLock();

    public void method() {
        //获得锁
        lock.lock();
        try {
            //方法体
        } finally {
            //释放锁
            lock.unlock();
        }
    }
}

5.3 Lock与synchronized比较:
Lock需要手动释放锁,使用比较灵活,只适用于代码块锁;synchronized不需要启动和释放锁,对象之间互斥。

6. 显示锁ReadWriteLock和ReentrantReadWriteLock

适用共享资源被大量读取操作,而只有少量的写操作。
ReentrantReadWriteLock是 ReadWriteLock在java.util里面的唯一的实现类。非公平锁,读线程无需锁竞争。
6.1 公平性:非公平锁(默认),读线程不存在锁之间的竞争,所以读线程没有公平性。公平锁会利用AQS的CLH队列释放当前保持的锁时选择等待时间长的写线程进行锁分配。非公平锁进行锁操作时可能立即获得锁推迟一个或多个读操作或者写操作。
6.2 重入行:写线程释放锁后,读写锁允许按照请求锁的顺序重新获得读取锁或者写入锁,写线程写入锁可以再次获得读取锁,读线程获取读取锁后不可以获取写入锁。
6.3 锁降级与升级:写线程获取写入锁后可以释放写入锁变成读取锁;获得写入锁需要释放所有读取锁,当两个读取锁共同获取写入锁,且不释放读取锁时会发生死锁,所以读取锁无法升级为写入锁。
总结:
读-读不互斥;读-写互斥;写-写互斥。
使用实例:

public class ReadAndWriteLock {
    static class Count{
        private final ReentrantReadWriteLock rwl = new ReentrantReadWriteLock();
        public void get(){
            //上读锁,其他线程只能读不能写,具有高并发性
            rwl.readLock().lock();
            try {
                System.out.println(Thread.currentThread().getName() + "read start.");
                Thread.sleep(1000L);
                System.out.println(Thread.currentThread().getName() + "read end.");
            } catch (InterruptedException e) {
                e.printStackTrace();
            }finally {
                //释放读锁
                rwl.readLock().unlock();
            }
        }

        public void put(){
            rwl.writeLock().lock();
            try {
                System.out.println(Thread.currentThread().getName() + "write start.");
                Thread.sleep(1000L);
                System.out.println(Thread.currentThread().getName() + "write end.");
            } catch (InterruptedException e) {
                e.printStackTrace();
            }finally {
                //释放写锁
                rwl.writeLock().unlock();
            }
        }

    }

    public static void main(String[] args) {
        final Count count = new Count();
        for (int i = 0; i < 2; i++) {
            new Thread(){
                @Override
                public void run(){
                    count.get();
                }
            }.start();
        }

        for (int i = 0; i < 2; i++) {
            new Thread(){
                @Override
                public void run(){
                    count.put();
                }
            }.start();
        }
    }
}

运行结果:读可以并发,写是有顺序阻塞。
请添加图片描述

6.4 ReentrantReadWriteLock与ReentrantLock比较:
相同点:都是显示锁,高并发;
不同点: ReentrantReadWriteLock是 ReentrantLock的扩展,实现读写分离的锁机制。

7. 显示锁 StampedLock

7.1 悲观锁与乐观锁
悲观锁:假定并发冲突一定会发生,屏蔽一切违反数据完整性的操作。
读取悲观锁:读取之前一定要判断一下数据是否被更改。
乐观锁:假设不会发生并发冲突,只在提交操作时检查是否违反数据完整性。
读取乐观锁:无需确定数据一致性
7.1.1 控制读写访问模式:
写:writeLock 所有乐观性读都将失败
读:readLock
乐观读

8. Synchronized、ReentrantLock、ReentrantReadWriteLock、StampedLock、对比

8.1 Synchronized是在JVM层面上的锁定,出现异常会自动释放锁。少量竞争者时可以使用,针对一个对象,其他为代码块层次锁定,必须将unLock()放到finally{}中。
8.2 ReentrantLock通用的锁的实现,简单的加锁解锁业务逻辑中。
8.3 ReentrantReadWriteLock对Lock的扩展,引入read、write阻塞和并发机制,实现业务相对更加复杂一些,并发性更高一些。
8.3 StampedLock在Lock基础上实现满足悲观锁、乐观锁等一些在读线程多的业务场景,改善吞吐量,理解不深入容易误用其他方法造成死锁等不必要麻烦。
8.4 不是业务需要,建议使用ReentrantLock和ReentrantReadWriteLock即可满足大部分业务场景需求。

9.死锁

两段不同逻辑等待对方释放锁才能继续执行,表面现象:程序执行不下去了。

volatile:告诉编译器,该变量是易变的,需要从内存读取,不能从缓存读,只提供内存可见性,没有提供原子性。
适用场景:对内存可见性要求高,原子性要求低,例如一个线程写,多个线程读该变量。

atomic:原子操作,线程加强版的volatile原子操作。利用cpu比较并交换(CAS)和非阻塞算法

评论
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

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

抵扣说明:

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

余额充值