前言
上一篇概述了一些线程的状态和方法,下面介绍一下线程安全。
线程安全
线程工作都有单独的工作空间,一般都是先拷值过来修改再更新值,存在更新值之前其他线程也进行了值拷贝,导致值存在不同步的问题,线程不安全。
线程并发三要素:
同一个对象
多个线程
同时操作
存在线程安全问题,于是就需要实现线程同步,线程同步的步骤有:形成队列、等待队列、加上锁(锁机制synchronized)。线程同步的任务是保证安全和性能。
synchronized
使用synchronized锁住资源,每次只允许一个线程对锁住的资源进行访问修改操作,保证资源的同步。
使用synchronized分为同步方法和同步块,用synchronized修饰的范围太大将会大大影响效率,比如一个方法内有A和B两个属性,A只读,B需同步写,如果用同步方法,则会影响读A的效率,用同步块来缩小同步范围。
线程不安全实例
package threadtest.syn;
import mainTest.Test;
import study_1.Main;
/**
* 购票不同步导致的问题
*/
public class UnsafeTest_01 {
public static void main(String[] args) {
UnsafeWeb12306 web12306 = new UnsafeWeb12306();
Thread t1 = new Thread(web12306,"农民");
Thread t2 = new Thread(web12306, "白领");
Thread t3 = new Thread(web12306, "中产");
Thread t4 = new Thread(web12306, "资产");
t1.start();
t2.start();
t3.start();
t4.start();
}
}
class UnsafeWeb12306 implements Runnable{
private int ticketNums = 10;
private boolean flag = true;
@Override
public void run() {
while (flag){
test();
}
}
public void test(){
if (ticketNums < 1){
setFlag(false);
}else {
//可能产生的问题
//1.当ticketNums=1线程进来时,休眠一会,这个时候还没有对值进行修改,
// 而这时有新的线程也通过ticketNums=1进来,
// 这个时候就会导致ticketNums被两个线程先后修改,出现ticketNums=0甚至负数的情况
//2.线程工作有单独的工作空间,先拷值过来修改再更新值,存在更新值之前其他线程也进行了值拷贝,
// 两个线程拷贝了相同的值,导致修改后打印的值相同,线程不安全。
try {
Thread.sleep(200);
} catch (InterruptedException e) {
e.printStackTrace();
}
System.out.println(Thread.currentThread().getName()
+ "-->" + ticketNums--);
}
}
public boolean isFlag() {
return flag;
}
public void setFlag(boolean flag) {
this.flag = flag;
}
}
线程不安全,出现了打印出票号小于0或者相同票号的情况
分别使用同步方法和同步块来解决线程安全问题
package threadtest.syn;
public class SyncTest_01 {
public static void main(String[] args) {
SafeWeb12306 web12306 = new SafeWeb12306();
Thread t1 = new Thread(web12306,"农民");
Thread t2 = new Thread(web12306, "白领");
Thread t3 = new Thread(web12306, "中产");
Thread t4 = new Thread(web12306, "资产");
t1.start();
t2.start();
t3.start();
t4.start();
}
}
class SafeWeb12306 implements Runnable{
//设置票数大一些,否则看不到效果
private int ticketNums = 100;
private boolean flag = true;
@Override
public void run() {
while (flag){
// test();
test2();
}
}
/**
* 同步方法实现线程同步
*/
public synchronized void test(){
if (ticketNums < 1){
setFlag(false);
}else {
try {
//模拟延时
Thread.sleep(20);
} catch (InterruptedException e) {
e.printStackTrace();
}
System.out.println(Thread.currentThread().getName()
+ "-->" + ticketNums--);
}
}
/**
* 同步块实现线程同步,需要控制ticketNums和flag,
* 所以同步块可以锁定他们所在的类对象即this,不可只锁其中一个变量。
*/
public void test2(){
synchronized(this) {
if (ticketNums < 1) {
setFlag(false);
} else {
try {
Thread.sleep(20);
} catch (InterruptedException e) {
e.printStackTrace();
}
System.out.println(Thread.currentThread().getName()
+ "-->" + ticketNums--);
}
}
}
public boolean isFlag() {
return flag;
}
public void setFlag(boolean flag) {
this.flag = flag;
}
}
synchronized锁的是资源,所以需要看清你需要锁的资源是哪一个,然后再对资源进行锁定。
synchronized(obj){} obj称之为同步监视器,一般为需要同步的资源,且注意obj是一个不变的资源对象(并非是属性不变,引用不变)。
juc包里面有写好了的同步容器。如;CopyOnWriteArrayList
同步带来的死锁问题
死锁:过多的同步可能造成相互不释放资源,从而互相等待,一般发生于同步中持有多个对象的锁。
如何避免:不要在同一个代码块中,同时持有多个对象的锁(不要嵌套使用synchronized)。
生产者和消费者模式:
共享同一个资源,相互依赖,互为条件。wait和notify方法(Object类的方法)只能在同步方法或块中执行,否则异常。
方法1:管程法,生产者和消费者之间有一个缓冲器,双方不需要相互打交道,只需要从缓冲区进行操作。
方法2:信号灯法。根据信号来分别进行操作。适用于一人一下的交互。
wait方法使线程进入阻塞状态,但会释放锁,等待其他线程当前同步块中调用notify方法唤醒。
管程法
package threadtest.locktest;
/**
* 协作模式,生产者消费者模式实现方式一:管程法。
* 管程法通过一个缓冲区来实现,生产者和消费者不需要和对方打交道。
* @author jjh
*/
public class CoTest01 {
public static void main(String[] args) {
Container container = new Container();
Productor productor = new Productor(container);
Consumer consumer = new Consumer(container);
productor.start();
consumer.start();
}
}
/**
* 生产者
*/
class Productor extends Thread{
private Container container;
public Productor(Container container) {
this.container = container;
}
@Override
public void run() {
for (int i = 0; i < 100; i++) {
System.out.println("生产第" + i + "个馒头");
container.push(new Steamedbun(i));
}
}
}
/**
* 消费者
*/
class Consumer extends Thread{
private Container container;
public Consumer(Container container) {
this.container = container;
}
@Override
public void run() {
for (int i = 0; i < 100; i++) {
System.out.println("消费序号为" + container.pop().id
+ "的馒头");
}
}
}
/**
* 缓冲区
*/
class Container {
/**
* 给定缓冲区大小
*/
Steamedbun[] steamedbuns = new Steamedbun[10];
/**
* 计数器
*/
int count = 0;
/**
* 生产
* @param bun
*/
public synchronized void push(Steamedbun bun){
//缓冲区已满 停止生产
if (count == steamedbuns.length){
try {
this.wait();
} catch (InterruptedException e) {
e.printStackTrace();
}
}
//开始生产
steamedbuns[count] = bun;
count++;
this.notifyAll();
}
/**
* 消费
* @return
*/
public synchronized Steamedbun pop(){
//缓冲区为空 停止消费
if (count == 0){
try {
this.wait();
} catch (InterruptedException e) {
e.printStackTrace();
}
}
count--;
Steamedbun bun = steamedbuns[count];
this.notifyAll();
return bun;
}
}
/**
* 面包
*/
class Steamedbun {
int id;
public Steamedbun(int id) {
this.id = id;
}
}
信号灯法
package threadtest.locktest;
/**
* 协作模式,生产者消费者模式实现方式一:管程法。
* @author jjh
*/
public class CoTest02 {
public static void main(String[] args) {
Tv tv = new Tv();
new Player(tv).start();
new Watcher(tv).start();
}
}
/**
* 播放节目
*/
class Player extends Thread{
Tv tv;
public Player(Tv tv) {
this.tv = tv;
}
@Override
public void run() {
for (int i = 0; i < 20; i++) {
if (i % 2 ==0){
tv.play("奇葩说");
}else {
tv.play("太污了,来包立白洗洗嘴!");
}
}
}
}
/**
* 观众
*/
class Watcher extends Thread{
Tv tv;
public Watcher(Tv tv) {
this.tv = tv;
}
@Override
public void run() {
for (int i = 0; i < 20; i++) {
tv.watch();
}
}
}
/**
* 电视
*/
class Tv {
/**
* 节目
*/
String voice;
/**
* 信号灯
* true表示演员表演
* false表示观众观看表演
*/
boolean flag = true;
public synchronized void play(String voice){
//演员等待
if (!flag){
try {
this.wait();
} catch (InterruptedException e) {
e.printStackTrace();
}
}
//可以开始表演了
System.out.println("表演了" + voice);
this.voice = voice;
this.notifyAll();
//标志位改变
this.flag = !this.flag;
}
public synchronized void watch(){
//观众等待
if (flag){
try {
this.wait();
} catch (InterruptedException e) {
e.printStackTrace();
}
}
//有节目开始观看
System.out.println("观看了" + voice);
this.notifyAll();
//标志位改变
this.flag = !this.flag;
}
}
指令重排
volatile
多线程存在指令重排造成代码结果不在期望之中,于是volatile的作用:
1)保证了不同线程对这个变量进行操作时的可见性,即一个线程修改了某个变量的值,这新值对其他线程来说是立即可见的。
2)禁止进行指令重排序。
volatile保证原子性,像轻量级的synchronized.
我们看一下经典的单例模式实现方式之懒汉式双重检查单例模式:
package threadtest.singletest;
/**
* DCL单例模式: 懒汉式套路基础上加入并发控制,保证在多线程环境下,对外存在一个对象
* 1、构造器私有化 -->避免外部new构造器
* 2、提供私有的静态属性 -->存储对象的地址
* 3、提供公共的静态方法 --> 获取属性
*/
public class DoubleCheckLocking {
//懒汉式,加上volatile,因为线程创建有三步
//1、new开辟空间 //2、构造 初始化对象信息 //3、返回对象的地址给引用
//防止指令重排序,2没执行完3便返回的引用。
private static volatile DoubleCheckLocking instance;
//私有的构造函数
private DoubleCheckLocking(){
}
/**
* 非同步
* @return
*/
public static DoubleCheckLocking getInstance1(Long time) {
if (null == instance){
try {
Thread.sleep(time);
} catch (InterruptedException e) {
e.printStackTrace();
}
instance = new DoubleCheckLocking();
}
return instance;
}
/**
* 双重检测的单例模式
* @return
*/
public static DoubleCheckLocking getInstance() {
//非空返回,不走同步块,提高效率
if (null != instance){
return instance;
}
synchronized (DoubleCheckLocking.class){
if (null == instance){
instance = new DoubleCheckLocking();
}
}
return instance;
}
public static void main(String[] args) {
// Thread t = new Thread(()->{
// System.out.println(DoubleCheckLocking.getInstance());
// }) ;
// t.start();
// System.out.println(DoubleCheckLocking.getInstance());
Thread t2 = new Thread(()->{
System.out.println(DoubleCheckLocking.getInstance1(1000L));
}) ;
t2.start();
System.out.println(DoubleCheckLocking.getInstance1(2000L));
}
}
锁还有一些其他的知识,概述一下:
锁分为两类:
悲观锁:synchronized是独占锁,会导致其他锁挂起。
乐观锁:每次不加锁而是假设没有冲突而去完成操作,如果冲突失败就重试,直到成功。
CAS:Compare and Swap 比较并交换,乐观锁的一种实现,属于硬件级的操作(利用CPU的CAS指令,同时借助JNI来完成非阻塞算法),效率比加锁高。
当前值V,将更新为的值B,旧的预期值A,通过比较V和A,要是相等就更新值为B,否则返回false,这种比较并交换的方式存在ABA的问题,
V和A比较并不能断定A没有变化,有可能是A->B->A,可以通过记录日志的方法来解决ABA问题,如果日志改变了说明值是发生过变化的。如AtomicInteger
可重入的锁(juc包ReentrantLock),当线程向一个已经拥有的锁申请锁时是可重入的,并不会因为被自己占用就申请不到而死锁,当重复申请时,锁计数会加一,只有计数为0才释放锁。
yield方法虽然进入就绪状态,但是不会释放锁,容易造成死锁。
结束
在此感谢尚学堂官网的视频,谢谢!
代码我打包了,需要的可以下载,仅供参考!