线程的相关理解就不多说了,直接进入主题:通过同步锁实现线程安全
在多线程,为了避免多个线程同时操作某个共享变量导致数据错乱,采用了同步锁机制,保证了操作的原子性,数据的一致性及线程安全。可以结合数据库的事务操作理解。
同步锁又可以分为对象锁,实例锁,类锁。
对象锁,锁住某个对象;实例锁,锁住某个实例,类锁,锁住某个类。
哈哈哈,佛了,有点抽象,不知道怎么解释,很尴尬。不急,可以根据下面的应用场景及代码来理解。
假定一个应用场景,就用经典的火车票售票系统为例:
假设有10张票,编号为1~10,有四个售票窗口(1~3)在卖,即3个窗口共享这10张票,总共卖出的票不能超出10张,也不能有相同号的票被卖出。
未加锁,线程不安全,代码如下:
/**
* 对象锁
*/
public class SyncObject {
static Object lock = new Object();
public static void main(String[] args) {
Thread t1 = new Thread(new MyRunnable(),"窗口1");
Thread t2 = new Thread(new MyRunnable(),"窗口2");
Thread t3 = new Thread(new MyRunnable(),"窗口3");
t1.start();
t2.start();
t3.start();
}
private static class MyRunnable implements Runnable{
static int a = 10; //类变量,多个实例共享,数据存在线程安全问题
@Override
public void run() {
sale0();
}
//无锁,线程不安全
public static void sale0(){
while (a>0){ //有票
try{
Thread.sleep(500); //模拟网络延迟,更能看出问题,多个线程在此处等待开始竞争
System.out.println(Thread.currentThread().getName()+"卖出"+(a--)+"号票");
}catch (Exception e){
}
}
}
}
}
运行结果:
窗口3卖出10号票
窗口2卖出9号票
窗口1卖出8号票
窗口3卖出7号票
窗口2卖出7号票
窗口1卖出6号票
窗口3卖出5号票
窗口2卖出5号票
窗口1卖出4号票
窗口3卖出3号票
窗口2卖出2号票
窗口1卖出1号票
窗口3卖出-1号票
窗口2卖出0号票
可见7号票,5号票卖了两次,只有10张票,3个窗口总共卖了14张票,数据错乱,线程不安全。
原因分析:
当a=1时,t1,t2,t3 都跑通了while(a>0),经过0.5s的sleep后,t1率先执行了
System.out.println(Thread.currentThread().getName()+"卖出"+(a--)+"号票");
此时,a=0,然后t2接着执行,a=-1,最后t3执行,即出现了超卖现象;
同理,当a=7时,线程t3率先执行了
System.out.println(Thread.currentThread().getName()+"卖出"+(a--)+"号票");
输出卖了7号票,还没来得及将a--赋值给a,这时,线程t2来了,也执行了:
System.out.println(Thread.currentThread().getName()+"卖出"+(a--)+"号票");
即,窗口2和3同时卖出了7号票。
这两种情况都是不允许发生的,于是对程序进行了改进,加入同步锁,保证线程安全:
/**
* 对象锁
*/
public class SyncObject {
static Object lock = new Object();
public static void main(String[] args) {
Thread t1 = new Thread(new MyRunnable(),"窗口1");
Thread t2 = new Thread(new MyRunnable(),"窗口2");
Thread t3 = new Thread(new MyRunnable(),"窗口3");
t1.start();
t2.start();
t3.start();
}
private static class MyRunnable implements Runnable{
static int a = 10; //共享变量,数据存在线程安全问题
@Override
public void run() {
sale1();
}
//线程安全,但不合理
//当某线程进入sale方法,获取到锁后,运行synchronized中代码块,while()循环,直到a为0,才运行完才释放锁
//这期间其他售票窗口无法进入,无法卖票,当a=0时,其他窗口才获取到该锁,才能执行synchronized代码块,
// 而此时已没票
public static void sale1(){
synchronized (lock){ //同步代码块,某一时刻只允许一个线程进来,运行完释放锁
while (a>0){
try{
Thread.sleep(500); //模拟买票网络延迟
System.out.println(Thread.currentThread().getName()+"卖出"+(a--)+"号票");
}catch (Exception e){
}
}
System.out.println(Thread.currentThread().getName()+"票已卖完。。。");
}
}
}
}
运行结果如下:
窗口1卖出10号票
窗口1卖出9号票
窗口1卖出8号票
窗口1卖出7号票
窗口1卖出6号票
窗口1卖出5号票
窗口1卖出4号票
窗口1卖出3号票
窗口1卖出2号票
窗口1卖出1号票
窗口1票已卖完。。。
窗口3票已卖完。。。
窗口2票已卖完。。。
多次运行后发现,虽然没有出现同号票,或多出的票,但是10张票都是只由一个窗口(假设1)卖完,其他窗口无票可卖。
导致的情况是,若窗口1票卖不完,而其他窗口想买买不到,造成了资源不能合理充分利用。
虽然加了sync同步锁,某个时刻只能有一个线程执行sync中的代码块,保证了变量a操作的原子性,数据的一致性。
但是由于while()循环,只要a>0有票,持有锁的线程就会一直执行,不释放锁,则其他线程只能等待,无法进行售票。
直到该线程执行完sync中代码块,才释放锁,其他线程才能抢到锁,但a已经为0,已无票可售。虽然保证了线程安全,
但是不能满足现实业务需求,我们需要的是在有票的情况下,各个窗口都有机会抢到票卖,既不能出现重复票,也不能
超卖。改进如下:
package com.knowsight.test.Thread.synclock;
/**
* 对象锁
*/
public class SyncObject {
static Object lock = new Object();
public static void main(String[] args) {
Thread t1 = new Thread(new MyRunnable(),"窗口1");
Thread t2 = new Thread(new MyRunnable(),"窗口2");
Thread t3 = new Thread(new MyRunnable(),"窗口3");
t1.start();
t2.start();
t3.start();
}
private static class MyRunnable implements Runnable{
static int a = 10; //共享变量,数据存在线程安全问题
@Override
public void run() {
//sale1();
sale2();
//sale0();
}
//无锁,线程不安全
public static void sale0(){
while (a>0){
try{
Thread.sleep(500); //模拟网络延迟,更能看出问题,多个线程在此处等待开始竞争
System.out.println(Thread.currentThread().getName()+"卖出"+(a--)+"号票");
}catch (Exception e){
}
}
}
//线程安全,但不合理
//当某线程进入sale方法,获取到锁后,运行synchronized中代码块,while()循环,直到a为0,才运行完才释放锁
//这期间其他售票窗口无法进入,无法卖票,当a=0时,其他窗口才获取到该锁,才能执行synchronized代码块,
// 而此时已没票
public static void sale1(){
synchronized (lock){ //同步代码块,某一时刻只允许一个线程进来,运行完释放锁
while (a>0){
try{
Thread.sleep(500); //模拟买票网络延迟
System.out.println(Thread.currentThread().getName()+"卖出"+(a--)+"号票");
}catch (Exception e){
}
}
System.out.println(Thread.currentThread().getName()+"票已卖完。。。");
}
}
//线程安全,较为合理
public static void sale2(){
while (a>0){ //有票,无锁,多个线程(售票窗口)可以同时进入
synchronized (lock){ //同步代码块,只能一个线程进入,运行完释放锁
if(a>0){ //因为当a=1时,可能会有多个线程跑通了a>0,在sync外等待,但只能给其中一个,需要再次判断是否有票
try{
Thread.sleep(500); //模拟买票网络延迟
System.out.println(Thread.currentThread().getName()+"卖出"+(a--)+"号票");
}catch (Exception e){
}
}else {
System.out.println(Thread.currentThread().getName()+"票已卖完。。。");
}
}
}
}
}
}
代码较简单,也有注释,就不多唠叨。
关于多线程,个人感觉比较抽象,每个人都有各自的理解。以上是个人以火车票售票系统作为应用场景,及代码的具体实现,来理解多线程,及线程安全,同步锁等,如有不妥之处,望多多指教!