如果多个线程同时运行,而这些线程同时具有修改某段代码的能力。程序不管是单线程执行还是多线程执行,结果都一样,并且变量与预期一样,就是线程安全。
线程安全问题都是全局变量或者静态变量导致的,如果每个线程中对全局变量、静态变量只有读操作,而没有写操作,通常这个全局变量是线程安全的;如果多个线程同时执行写操作,就需要考虑到线程同步问题,否则就是出现线程安全问题。
举一个例子,假设火车票卖票,有3个窗口同时卖票,库存10张,那么三个独立窗口,各自都会判断余票必须大于0,否则买票不成功,我们就假设每次只卖1张。
public class Ticket implements Runnable{
private int ticket=10;
@Override
public void run() {
do{
if(ticket>0){
try {
Thread.sleep(1000);
} catch (InterruptedException e) {
e.printStackTrace();
}
ticket--;
System.out.println("售出1张 success");//每次成功售出都打印一下
System.out.println("窗口:"+Thread.currentThread().getName()+",余票:"+ticket);
}
}while(ticket>0);
}
}
启动卖票,很显然余票已经显示负数了,存在超售问题。

而当我们只启动一个线程(一个窗口),可以正常售卖,不会超售。

当判断ticket大于0时,多个窗口都能同时满足条件,于是大家走执行了扣减1张的操作,于是当余票为1时,三个窗口都扣减了1次,就变成了-2张。
当我们使用多线程访问同一资源的时候,且多个线程都有写操作(扣减或者增加),就容易出现线程安全问题。要解决这个多线程并发访问同一资源的安全问题,Java提供了同步锁机制来解决synchronized。
实现逻辑:
窗口1线程进入操作的时候,其它窗口等待,当窗口1结束,其它窗口可以进行操作。也就是当某线程操作共享资源的时候,其它线程不能修改资源,等待修改同步完成,才能获取该资源进行执行,保证数据的同步性,解决了线程不安全问题。
为了保证每个线程都可以进行原子性的操作,Java引入了同步锁机制。
synchronized 只在方法的某一部分进行操作,表示多个线程在执行这段代码时,需要等待锁释放,对象锁。
格式
synchronized(同步锁){
//共享资源的写操作,对共享资源进行判断和修改
}
同步锁: 对象的同步锁只是一个概念,可以理解为在对象上标记一个锁。
package com.lengcz.tools;
public class Ticket implements Runnable{
private int ticket=10;
private Object lock=new Object();
@Override
public void run() {
do{
synchronized(lock){
if(ticket>0){
try {
Thread.sleep(1000);
} catch (InterruptedException e) {
e.printStackTrace();
}
ticket--;
System.out.println("售出1张 success");//每次成功售出都打印一下
System.out.println("窗口:"+Thread.currentThread().getName()+",余票:"+ticket);
}
}
}while(ticket>0);
}
}

使用synchronized修饰方法,就叫同步方法,多个线程同时执行同步方法时,获取锁的线程可以这个方法的执行权限,其它线程只能等待这个线程完成同步方法的执行,才能争抢同步方法的执行权限,任何时候都最多只有一个线程有执行同步锁方法的权限。
public synchronized void method(xxx){
//共享资源的写操作,对共享资源进行判断和修改
}
package com.lengcz.tools;
public class Ticket implements Runnable{
private int ticket=10;
@Override
public void run() {
sell();
}
public synchronized void sell(){
do{
if(ticket>0){
try {
Thread.sleep(1000);
} catch (InterruptedException e) {
e.printStackTrace();
}
ticket--;
System.out.println("售出1张 success");//每次成功售出都打印一下
System.out.println("窗口:"+Thread.currentThread().getName()+",余票:"+ticket);
}
}while(ticket>0);
}
}

我们让窗口1执行Ticket 1的售票作业,窗口4 执行Ticket 2售卖作业,这两个售票作业没有关系,资源不共享。

运行代码,我们发现两个售票窗口都在同时执行各自的卖票作业,如果同步方法锁独享方法资源,那么卖出20张票需要20秒,而实际上它们各自卖票,所以t1和t2并没有竟然同一个资源,那么它们实际上是
synchronized(this),即synchronized(ticket1)和synchronized(ticket2)。
结论:

public synchronized static void method(xxx){
//code
}
静态方法的同步锁到底是锁什么?
我们改造一下卖票demo,只让进入静态同步方法的线程打印一下自己的身份(线程名)和执行时间。
import java.util.Date;
public class Ticket2 implements Runnable{
@Override
public void run() {
try {
do{
sell();
}while(true);
} catch (InterruptedException e) {
// TODO Auto-generated catch block
e.printStackTrace();
}
}
public synchronized static void sell() throws InterruptedException{
Thread.sleep(1000);
System.out.println("sell窗口:"+Thread.currentThread().getName()+",获取到执行资源:"+DateUtils.parseDate(new Date()));
}
public synchronized static void sell2() throws InterruptedException{
Thread.sleep(1000);
System.out.println("sell2窗口:"+Thread.currentThread().getName()+",获取到执行资源:"+DateUtils.parseDate(new Date()));
}
}
public class Ticket3 implements Runnable{
@Override
public void run() {
try {
do{
// sell();
Ticket2.sell2();
}while(true);
} catch (InterruptedException e) {
// TODO Auto-generated catch block
e.printStackTrace();
}
}
}

执行demo

线程1和线程4同时竞争都竞争sell方法(静态同步方法),而观察日志,发现1和4并不能同时执行,说明线程1和线程4访问该方法时,存在竞争关系,但是我们给t1和t4传入的是不同的Ticket2对象,显然不可能是对象锁,那么它们用的就是synchronized(Ticket2.class) ,锁的是该类的字节码对象。

线程1和线程5对比,两者并没有竞争同一个方法,也没有竞争通过一个Ticket对象,线程1调用sell方法,线程5则调用sell2方法,两者没有交集。但是我们观察上面的打印日志,可以看到,线程1和线程5并没有同时执行,它们各自执行时,都独占了资源。这说明它们这个锁是针对的Ticket2.class对象的,synchronized(Ticket2.class),该类的不同静态同步方法也存在竞争关系。


同步方法锁this,而静态同步方法锁类这个类本身,该类的静态同步方法存在锁竞争关系。
java.util.concurrent.locks.Lock锁提供了比synchronized更细化的锁定操作,同步方法块和同步方法具有的Lock都有,此外,Lock更体现面对对象。
Lock锁也称为同步锁。
Lock.lock();#上锁
Lock.unlock();# 释放锁
使用示例
import java.util.concurrent.locks.Lock;
import java.util.concurrent.locks.ReentrantLock;
public class LockDemo {
Lock lock=new ReentrantLock();
public void method(){
lock.lock();
/**
* 需要加锁保护的代码
*/
lock.unlock();
}
}
我们使用Lock锁修改卖票操作。
import java.util.Date;
import java.util.concurrent.locks.Lock;
import java.util.concurrent.locks.ReentrantLock;
public class Ticket implements Runnable{
private int ticket=10;
Lock lock=new ReentrantLock();
@Override
public void run() {
String name = Thread.currentThread().getName();
try {
do {
lock.lock();// 上锁
if (ticket > 0) {
Thread.sleep(1000);//模拟业务处理需要1秒
System.out.println("窗口:" + name + ", 正在运行....");
ticket--;
System.out.println("售出1张 success,time:" + DateUtils.parseDate(new Date()));// 每次成功售出都打印一下
System.out.println("窗口:" + name + ",余票:" + ticket);
lock.unlock();// 释放锁
}
Thread.sleep(1);//休息1ms是为了避免该线程while循环立即再次lock,让出资源给其它线程
} while (ticket > 0);
} catch (Exception e) {
System.out.println("窗口:" + name + ", error ....");
e.printStackTrace();
}
}
}
启动t1,t2,t3线程进行卖票作业,发现被Lock锁的代码同样达到了synchronized锁的效果。

由于使用Lock锁,需要手动lock和unlock,因此如果在业务处理发生异常时,unlock可能无法被执行。
import java.util.Date;
import java.util.concurrent.locks.Lock;
import java.util.concurrent.locks.ReentrantLock;
public class Ticket implements Runnable{
private int ticket=10;
Lock lock=new ReentrantLock();
@Override
public void run() {
String name = Thread.currentThread().getName();
try {
do {
lock.lock();// 上锁
if (ticket > 0) {
Thread.sleep(1000);//模拟业务处理需要1秒
System.out.println("窗口:" + name + ", 正在运行....");
if(name.equals("3")){
int a=1/0; //模拟某线程处理业务时发生异常,直接进入catch,不能执行unlock
}
ticket--;
System.out.println("售出1张 success,time:" + DateUtils.parseDate(new Date()));// 每次成功售出都打印一下
System.out.println("窗口:" + name + ",余票:" + ticket);
lock.unlock();// 释放锁
}
Thread.sleep(1);//休息1ms是为了避免该线程while循环立即再次lock,让出资源给其它线程
} while (ticket > 0);
} catch (Exception e) {
System.out.println("窗口:" + name + ", error ....");
e.printStackTrace();
}
}
}
上面窗口3在卖票时,正在卖票,但是发生了异常,Lock锁无法进行unlock,这个锁也就无法释放了,导致死锁问题。

所以在使用Lock锁时,一定要确保锁可以被释放。