线程安全

在多线程中,线程的执行是按照时间片划分的,从而线程的执行顺序具有随机性.当多个线程去访问共享的数据时,可能得到的结果和预期的结果不一致,产生线程安全问题.我们不能在提高系统效率的同时而忽略线程的安全问题,或者说,我们应该在保证线程安全的情况下,尽量的提升系统的效率.

一. 线程安全的判断

我们要判断一个程序是否是线程安全的,只需要看3个条件:

  1. 是不是多线程
  2. 是否存在共享资源
  3. 是否存在有多个线程同时访问共享资源

如果上述三个条件都存在,那么这个程序就存在线程安全的问题

二. 如何保证线程安全

保证线程安全的方式有很多,本文主要介绍加锁的方式,通过锁来保证线程安全.

将程序中共享的资源上锁,只有获得锁标记的线程才可以访问共享资源,其他需要访问资源但是没有获得锁的线程会阻塞到锁外,只有等到访问共享资源的线程访问完毕,释放锁,其他线程才可以去争夺锁,进而访问资源.

  1. synchronized (obj){}同步代码块

同步锁,在同一个时间只有一个线程可以访问上锁的资源.锁对象可以是任意的Object类型,将共享的代码包含在synchronized (obj){}的花括号内,这样括号内的内容就是一个不可分割的原子操作,实现线程安全

模拟电影院买票的实例,三个窗口负责卖100张电影票,明显存在线程安全问题,不解决线程安全,将会出现重票,票序紊乱.为了解决线程安全问题,我们将共享的代码上锁.如下:

package gw;

class MyRun implements Runnable {
	Object obj = new Object();
	private static int n = 100;

	@Override
	public void run() {
		// 模拟电影院买票
		while (n > 0) {
			synchronized (obj) {
				System.out.println(Thread.currentThread().getName()
				 + "卖出第" + n-- + "张票");
			}
		}
	}

}

public class Test6 {

	public static void main(String[] args) {
		// 创建任务对象
		Runnable r = new MyRun();
		// 将任务提交与3个线程
		Thread t1 = new Thread(r, "窗口一");
		Thread t2 = new Thread(r, "窗口二");
		Thread t3 = new Thread(r, "窗口三");
		// 启动3个线程,并发执行
		t1.start();
		t2.start();
		t3.start();
	}
}

  1. synchronized同步方法
    当一个方法中的所有代码都是共享资源,我们就可以将这个方法设置为同步方法.
    如下:将电影院买票的例子改为同步方法.效果和同步代码块一样.

package gw;

class MyRun2 implements Runnable {
	private static int n = 100;

	private synchronized void mon() {
		// 模拟电影院买票
		while (n > 0) {
			System.out.println(Thread.currentThread().getName() 
			+ "卖出第" + n-- + "张票");
		}
	}

	@Override
	public void run() {
		mon();
	}
}

public class Test7 {

	public static void main(String[] args) {
		// 创建任务对象
		Runnable r = new MyRun2();
		// 将任务提交与3个线程
		Thread t1 = new Thread(r, "窗口一");
		Thread t2 = new Thread(r, "窗口二");
		Thread t3 = new Thread(r, "窗口三");
		// 启动3个线程,并发执行
		t1.start();
		t2.start();
		t3.start();
	}
}

  1. Lock接口与ReentrantLock

java.util.concurrent.locks 包下的Lock接口.

Lock接口提供了更为灵活的加锁方式,功能更加全面,缺点是需要手动释放锁 .
Lock接口的常用方法:

项目Value
void lock()获得锁
void unlock()释放锁
boolean tryLock()尝试获得锁

lock()获得锁,与synchronized(obj){的功能一样,
而synchronized(obj){}在运行结束时会自动释放锁,但是lock()获得的锁需要使用unLock()方法释放锁

Lock多了一个常用的功能,tryLock(),尝试获得锁,在之前,当一个线程需要获得锁已经被其他线程获取时,当前线程就会阻塞自己,等待锁资源.而使用tryLock尝试获得锁时,如果没有获得锁,当前线程并不会阻塞自己,而是去做其他事情,等到一定时间再去尝试获得锁.

Lock是一个接口,不能直接实例化,我们通常使用其实现子类ReentrantLock实例化.

使用Lock接口加锁实例:

package gw;

import java.util.concurrent.locks.Lock;
import java.util.concurrent.locks.ReentrantLock;

class MyRun3 implements Runnable {
	// 创建一个Lock对象.
	Lock lock = new ReentrantLock();
	private static int n = 100;

	@Override
	public void run() {
		lock.lock();// 获得锁
		// 模拟电影院买票
		while (n > 0) {
			System.out.println(Thread.currentThread().getName() 
			+ "卖出第" + (n--) + "张票");
		}
		lock.unlock();// 释放锁
	}
}

public class Test8 {

	public static void main(String[] args) {
		// 创建任务对象
		Runnable r = new MyRun3();
		// 将任务提交与3个线程
		Thread t1 = new Thread(r, "窗口一");
		Thread t2 = new Thread(r, "窗口二");
		Thread t3 = new Thread(r, "窗口三");
		// 启动3个线程,并发执行
		t1.start();
		t2.start();
		t3.start();
	}
}
  1. ReadWriteLock接口与ReentrantReadWriteLock

java.util.concurrent.locks 包下的ReadWriteLock接口,将读锁与写锁设置不同的锁.

在学习读写锁之前,如果一个程序中的读操作远远大于写操作的话,为了线程安全又需要对其上锁,而读与读之间实际上是不会影响线程安全的,因此在这种情况下我们可以采取读写锁,来提高程序的运行效率.

我们通过下面的例子来说明读写锁的效率:
使用重入锁:
代码如下:

`package gw;

import java.util.concurrent.Callable;
import java.util.concurrent.ExecutorService;
import java.util.concurrent.Executors;
import java.util.concurrent.locks.Lock;
import java.util.concurrent.locks.ReentrantLock;

class Student {
	private String name;
	// 创建重入互斥锁对象
	Lock lock = new ReentrantLock();

	// 写的方法,执行一次休眠1秒
	public void setName(String name) {
		lock.lock();
		try {
			try {
				Thread.sleep(1000);
			} catch (InterruptedException e) {

				e.printStackTrace();
			}
			this.name = name;
		} finally {
			lock.unlock();
		}
	}

	// 读的方法,执行一次休眠1秒
	public String getName() {
		lock.lock();
		try {
			try {
				Thread.sleep(1000);
			} catch (InterruptedException e) {
				e.printStackTrace();
			}
			return name;
		} finally {
			lock.unlock();
		}
	}
}

public class Test9 {

	public static void main(String[] args) {
		System.out.println("程序开始!");
		Student s = new Student();
		Callable ca1 = new Callable() {
			// 执行写的任务
			@Override
			public Object call() throws Exception {
				s.setName("小白");
				return null;
			}
		};
		Callable ca2 = new Callable() {
			// 执行读的任务
			@Override
			public Object call() throws Exception {
				s.getName();
				return null;
			}
		};
		// 获取当前系统时间毫秒值
		Long start = System.currentTimeMillis();
		// 创建线程池
		ExecutorService es = Executors.newFixedThreadPool(25);
		// 提交写的任务3次
		for (int i = 0; i < 3; i++) {
			es.submit(ca1);
		}
		// 提交读的任务22次
		for (int i = 0; i < 22; i++) {
			es.submit(ca2);
		}

		es.shutdown();// 关闭线程池

		// 判断线程池的线程是否已经结束
		while (true) {
			if (es.isTerminated()) {
				break;
			}
		}
		// 获取程序运行结束时间的毫秒值
		Long end = System.currentTimeMillis();
		System.out.println("程序结束,运行时间" + (end - start) + "毫秒");

	}}
`

运行效果:

程序开始!
程序结束,运行时间25015毫秒

由于读写的方法都设置了休眠1秒,所以程序用时25秒左右.

实际上在读与读之间是可以并发执行的,我们采用读写锁来测试系统效率:

在java.util.concurrent.locks包下,有一个ReadWriteLock接口,这个接口提供了两个方法来获取读锁和写锁.

项目Value
public ReentrantReadWriteLock.ReadLock readLock()返回一个可重入的读锁
public ReentrantReadWriteLock.WriteLock writeLock()返回一个可重入的写锁

由于其返回类型是可重入的读写锁子实现类,所以我们直接使用其子类,ReentrantReadWriteLock创建一个可重入的读写锁:
**

  • // 创建一个可重入读写锁
  • ReentrantReadWriteLock rwl = new ReentrantReadWriteLock();
  • ReadLock rl = rwl.readLock();// 获取读锁
  • WriteLock wl = rwl.writeLock();// 获取写锁
package gwgw;

import java.util.concurrent.Callable;
import java.util.concurrent.ExecutorService;
import java.util.concurrent.Executors;
import java.util.concurrent.locks.Lock;
import java.util.concurrent.locks.ReentrantLock;
import java.util.concurrent.locks.ReentrantReadWriteLock;
import java.util.concurrent.locks.ReentrantReadWriteLock.ReadLock;
import java.util.concurrent.locks.ReentrantReadWriteLock.WriteLock;

class Student {
	private String name;
	// 创建重入互斥锁对象
	// Lock lock = new ReentrantLock();
	// 创建一个可重入读写锁
	ReentrantReadWriteLock rwl = new ReentrantReadWriteLock();
	ReadLock rl = rwl.readLock();// 获取读锁
	WriteLock wl = rwl.writeLock();// 获取写锁

	// 写的方法,执行一次休眠1秒
	public void setName(String name) {
		wl.lock(); // 加写锁
		try {
			try {
				Thread.sleep(1000);
			} catch (InterruptedException e) {

				e.printStackTrace();
			}
			this.name = name;
		} finally {
			wl.unlock();// 释放锁
		}
	}

	// 读的方法,执行一次休眠1秒
	public String getName() {
		rl.lock();// 加读锁
		try {
			try {
				Thread.sleep(1000);
			} catch (InterruptedException e) {
				e.printStackTrace();
			}
			return name;
		} finally {
			rl.unlock();// 释放锁
		}
	}
}

public class Test10 {

	public static void main(String[] args) {
		System.out.println("程序开始!");
		Student s = new Student();
		Callable ca1 = new Callable() {
			// 执行写的任务
			@Override
			public Object call() throws Exception {
				s.setName("小白");
				return null;
			}
		};
		Callable ca2 = new Callable() {
			// 执行读的任务
			@Override
			public Object call() throws Exception {
				s.getName();
				return null;
			}
		};
		// 获取当前系统时间毫秒值
		Long start = System.currentTimeMillis();
		// 创建线程池
		ExecutorService es = Executors.newFixedThreadPool(25);
		// 提交写的任务3次
		for (int i = 0; i < 3; i++) {
			es.submit(ca1);
		}
		// 提交读的任务22次
		for (int i = 0; i < 22; i++) {
			es.submit(ca2);
		}

		es.shutdown();// 关闭线程池

		// 判断线程池的线程是否已经结束
		while (true) {
			if (es.isTerminated()) {
				break;
			}
		}
		// 获取程序运行结束时间的毫秒值
		Long end = System.currentTimeMillis();
		System.out.println("程序结束,运行时间" + (end - start) + "毫秒");
	}
}

执行结果:

程序开始!
程序结束,运行时间4006毫秒

可见程序只用了4秒,3次写操作用时3秒,剩余的22次读操作并发执行,用时1秒,大大提高了系统效率.

三. 线程安全的集合

在这里插入图片描述

每一个线程不安全的集合会对应一个线程安全的版本

项目Value
ArrayListCopyOnWriteArrayList
SetCopyOnWriteArraySet
HashMapConcurrentHashMap
QueueConcurentLinkedQueue
阻塞QueueArrayBlockingAueue
  • 4
    点赞
  • 6
    收藏
    觉得还不错? 一键收藏
  • 0
    评论

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

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

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

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

抵扣说明:

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

余额充值