前言
小咸儿在上一篇多线程——线程通讯中,提到线程安全问题,今天就来说一说。
叙述
宝图
先来一张导图来看看线程安全的分布?
线程安全
线程安全是什么呢?
当多个线程共享同一个全局变量,做写操作时,可能会收到其他线程的干扰,做读操作则不会发生线程安全。
解决方法
既然遇到了线程安全问题,那么该如何解决这个问题呢?
这时候就会先考虑到两个关键字:volatile 和 synchronized
volatile关键字: 保证可见性的问题。保证另一个线程可见,及时将修改后的变量刷新到主内存中。虽然能够保证可见性,但是无法保证原子性。
代码展示:
package com.practice.demo.thread.lock;
import java.util.HashMap;
import java.util.Map;
import java.util.concurrent.locks.ReentrantReadWriteLock;
public class ReadWriteLock {
private volatile Map<String, String> caChe = new HashMap<>();
/**
* 读写锁
*/
private ReentrantReadWriteLock rwl = new ReentrantReadWriteLock();
/**
* 写入锁
*/
private ReentrantReadWriteLock.WriteLock writeLock = rwl.writeLock();
private ReentrantReadWriteLock.ReadLock readLock = rwl.readLock();
/**
* 写入元素
* @param key key值
* @param value value值
*/
public void put(String key, String value){
try {
writeLock.lock();
System.out.println("写入put方法key:" + key + ",value:" + value + ".开始");
Thread.currentThread().sleep(100);
caChe.put(key,value);
System.out.println("写入put方法key:" + key + ",value:" + value + ".结束");
} catch (Exception e){
}finally {
writeLock.unlock();
}
}
/**
* 读取元素
* @param key key值
*
*/
public String get(String key){
try {
readLock.lock();
System.out.println("读取key:" + key + ".开始");
Thread.currentThread().sleep(100);
String value = caChe.get(key);
System.out.println("读取key:" + key + ".结束");
return value;
}catch (Exception e){
return null;
}finally {
readLock.unlock();
}
}
public static void main(String[] args){
ReadWriteLock readWriteLock = new ReadWriteLock();
// 写线程
Thread t1 = new Thread(new Runnable() {
@Override
public void run() {
for (int i = 0; i < 10; i++){
readWriteLock.put("i", i + "");
}
}
});
// 读线程
Thread t2 = new Thread(new Runnable() {
@Override
public void run() {
for (int i = 0; i < 10; i++){
readWriteLock.get(i + "");
}
}
});
t1.start();
t2.start();
}
}
打印结果:
结果: 即当这个线程开始和结束都执行完之后,另一个线程才能进行,并且全局变量发生变化后对另一个线程可见。
synchronized关键字: 内置锁,保证线程原子性,当线程进入方法的时候,自动获取锁,一旦锁被某个线程获取到后,其他的线程就会等待。即可修饰代码块也可用来修饰方法。
代码展示
public void run() {
// 使用account作为同步监视器,任何线程进入下面同步代码块之前
// 必须先获得对account账户的锁定——其他线程无法获得锁,也就无法修改她
// 这种做法符合:“加锁——修改——释放锁”的逻辑
synchronized (account) {
// 账户余额大于取钱数目
if (account.getBalance() >= drawAmount) {
// 吐出钞票
System.out.println(getName() + "取钱成功!吐出钞票:" + drawAmount);
try {
Thread.sleep(1);
} catch (InterruptedException ex) {
ex.printStackTrace();
}
// 修改余额
account.setBalance(account.getBalance() - drawAmount);
System.out.println("\t余额为:" + account.getBalance());
} else {
System.out.println(getName() + "取钱失败,余额不足!");
}
}
}
区别
volatile和synchronized的区别:
-
volatile不会进行加锁操作:
volatile变量是一种稍弱的同步机制在访问volatile变量时不会执行加锁操作,因此也就不会执行线程阻塞,因此volatile变量是一种比synchronized关键字更加轻量级的同步机制。 -
volatile不如synchronized安全:
在代码中如果过度依赖volatile变量来控制状态的可见性,通常会比使用锁的代码更脆弱,也更加难以理解。仅当volatile变量能简化代码的实现以及对同步策略的验证时,才应该使用它。一般来说,使用同步机制会更安全。 -
volatile变量作用类似于同步变量读写操作:
从内存可见性的角度看,写入volatile变量相当于退出同步代码块,而读取volatile变量相当于进入同步代码块。 -
volatile无法同时保证内存可见性和原子性:
加锁机制既可以确保可见性又可以确保原子性,而volatile变量只能确保可见性,原因是声明为volatile的简单变量如果当前值与该变量以前的值相关,那么volatile关键字不起作用,也就是说如下的表达式都不是原子操作:“count++”,“count = count + 1”。
问题
解决线程安全的问题不仅只有这两种处理办法,还有一种锁(Lock),具体且听小咸儿下回分享。
总结
多线程是十分实用并且常用的内容,接下来小咸儿还会继续深入学习多线程,更多的内容等待更新。
感谢您的阅读~~