线程安全问题:
在多线程中,有可能出现多个线程同时使用同一个资源的情况,这个资源可以是变量,数据表,txt文件等。这个资源称作"临界资源"
举个例子:取钱这个线程分为三个步骤:1.读取金额、2.取款、3.更新金额
有个典型的线程安全的例子,倘若A,B两人使用同一个账户(1000元)取款,执行顺序如下:
1.A读取金额,显示1000
2.A取款300元
3.B读取金额,显示为1000(实际应该为700)
4.B取款1000元
5.A更新金额为700
6.B更新金额0
之所以账户中莫名地被多取了300,是因为A和B两个线程使用了同一个临界资源(账户)
注意:当多个线程执行同一个方法时,方法内部的变量不是临界资源,因为每个线程都有自己独立的内存区域(PC,方法栈,线程栈)
解决线程安全问题:
基本所有的并发方案,都采用“序列化访问资源”,也就是在同一时间,只有一个线程能访问临界资源,也称作同步互斥访问
方案1:synchronized
在Java中,每个对象都有一个锁标记,称为monitor(监视器),多个线程访问这个对象的临界资源时,只有获取了该对象的锁才能访问
在Java中,可以使用synchronized关键字来标记一个方法或者代码块,当某个线程调用该对象的synchronized方法或者访问synchronized代码块时,这个线程便获得了该对象的锁,其他线程暂时无法访问这个方法,只有等待这个方法执行完毕或者代码块执行完毕,这个线程才会释放该对象的锁,其他线程才能执行这个方法或者代码块。
public class ThreadSynchronized2 {
public static void main(String[] agrs) {
InsertData insertData = new InsertData();
new Thread("线程1") {
@Override
public void run() {
insertData.insert(Thread.currentThread());
}
}.start();
new Thread("线程2") {
@Override
public void run() {
insertData.insert(Thread.currentThread());
}
}.start();
}
}
class InsertData extends Thread {
public synchronized void insert(Thread thread) {
for (int i = 0; i < 5; i++) {
System.out.println(thread.getName() + "插入: " + i);
}
}
}
输出:
线程1插入: 0
线程1插入: 1
线程1插入: 2
线程1插入: 3
线程1插入: 4
线程2插入: 0
线程2插入: 1
线程2插入: 2
线程2插入: 3
线程2插入: 4
还可以改成以下两种方式:
class InsertData extends Thread {
public void insert(Thread thread) {
synchronized (this) {
for (int i = 0; i < 5; i++) {
System.out.println(thread.getName() + "插入: " + i);
}
}
}
}
class InsertData extends Thread {
private Object object = new Object();
public void insert(Thread thread) {
synchronized (object) {
for (int i = 0; i < 5; i++) {
System.out.println(thread.getName() + "插入: " + i);
}
}
}
}
说明:
1)当一个线程正在访问一个对象的synchronized方法,那么其他线程不能访问该对象的其他synchronized方法。这个原因很简单,因为一个对象只有一把锁,当一个线程获取了该对象的锁之后,其他线程无法获取该对象的锁,所以无法访问该对象的其他synchronized方法。
2)当一个线程正在访问一个对象的synchronized方法,那么其他线程能访问该对象的非synchronized方法。这个原因很简单,访问非synchronized方法不需要获得该对象的锁,假如一个方法没用synchronized关键字修饰,说明它不会使用到临界资源,那么其他线程是可以访问这个方法的,
3)如果一个线程A需要访问对象object1的synchronized方法fun1,另外一个线程B需要访问对象object2的synchronized方法fun1,即使object1和object2是同一类型的对象,也不会产生线程安全问题,因为他们访问的是不同的对象,所以不存在互斥问题。
注意事项:
static方法,使用的是类锁(多个对象使用同一个类的monitor)
和非static方法,使用的是对象锁(每个对象维护一个monitor)
synchronized的优缺点:
1.使用synchronized包住的代码块,只可能有两种状态:顺利执行完毕释放锁、执行时发生异常释放锁,不会由于异常导致出现死锁现象
2.如果synchronized包住的代码块中有sleep等操作,比如I/O阻塞,但是其他线程还是需要等待,这样程序的效率就比较低了
3.等待synchronized释放锁的线程会一直等待下去(死心塌地,不到黄河心不死)
举例场景:
对a.txt这个文件,A,B线程都可以进行读写,写操作与写操作会发生冲突,写操作与读操作会发生冲突,但是,读操作与读操作不会发生冲突
这个时候synchronized满足不了需求,就需要用高级的锁
锁的介绍:
先看JDK中对于Lock的定义:
public interface Lock {
void lock();
void lockInterruptibly() throws InterruptedException;
boolean tryLock();
boolean tryLock(long time, TimeUnit unit) throws InterruptedException;
void unlock();
Condition newCondition();
}
通过Lock的源码可以知道,lock(),tryLock(),tryLock(long time,Time Unit),lockInterruptiably()是用来获取锁的
lock()方法是平常使用得最多的一个方法,用来获取锁,如果锁已经被其他线程获取,则进行等待,可以说和synchronized没有差别
Lock lock = ...;
lock.lock();
try{
//处理任务
}catch(Exception ex){
}finally{
lock.unlock(); //释放锁
}
实际例子:
public class ThreadLock {
private ReentrantLock lock = new ReentrantLock();
public static void main(String[] args) {
ThreadLock mt = new ThreadLock();
new Thread(() -> mt.insert()).start();
new Thread(() -> mt.insert()).start();
}
public void insert() {
lock.lock();
try {
//睡眠10s,等待锁的线程此时挂起
System.out.println(Thread.currentThread().getName() + "获得锁");
Thread.currentThread().sleep(10000);
} catch (Exception e) {
} finally {
System.out.println(Thread.currentThread().getName() + "释放锁");
lock.unlock();
}
}
}
输出:
Thread-0获得锁
Thread-0释放锁
Thread-1获得锁
Thread-1释放锁
tryLock()是有返回值的,且会立即返回,不会一直等待,如果获取到锁,则返回true,否则返回false
public class ThreadLock2 {
private ReentrantLock lock = new ReentrantLock();
public static void main(String[] args) {
ThreadLock2 mt = new ThreadLock2();
new Thread(() -> mt.insert(Thread.currentThread())).start();
new Thread(() -> mt.insert(Thread.currentThread())).start();
}
public void insert(Thread thread) {
if (lock.tryLock()) {
try {
System.out.println(thread.getName() + "得到了锁");
} catch (Exception e) {
} finally {
try {
Thread.sleep(10000);
} catch (InterruptedException e) {
e.printStackTrace();
}
System.out.println(thread.getName() + "释放了锁");
lock.unlock();
}
} else {
System.out.println(thread.getName() + "获取锁失败");
}
}
}
输出:
Thread-0得到了锁
Thread-1获取锁失败
Thread-0释放了锁
lockInterruptibly允许在等待时由其它线程调用等待线程的Thread.interrupt方法来中断等待线程的等待而直接返回,这时不用获取锁,而会抛出一个InterruptedException。
lock方法不允许Thread.interrupt中断,即使检测到Thread.isInterrupted,一样会继续尝试获取锁,失败则继续休眠。只是在最后获取锁成功后再把当前线程置为interrupted状态,然后再中断线程。
示例代码:
public class ThreadLock3 {
private static Lock lock = new ReentrantLock();
public static void main(String[] args) throws InterruptedException {
MyThread thread1 = new MyThread("1", lock);
MyThread thread2 = new MyThread("2", lock);
thread1.start();
Thread.sleep(100);
thread2.start();
Thread.sleep(1000);
thread2.interrupt();
}
}
class MyThread extends Thread {
private Lock lock = null;
public MyThread(String name, Lock lock) {
super(name);
this.lock = lock;
}
@Override
public void run() {
//注意,如果需要正确中断等待锁的线程,必须将获取锁放在外面,然后将InterruptedException抛出
try {
lock.lockInterruptibly();
out.println(this.getName() + "得到了锁");
long startTime = System.currentTimeMillis();
for (; ; ) {
if (System.currentTimeMillis() - startTime >= Integer.MAX_VALUE) {
break;
}
}
} catch (InterruptedException e) {
out.println(Thread.currentThread().getName() + "被中断");
} finally {
out.println(Thread.currentThread().getName() + "执行finally");
}
}
}
输出:
1得到了锁
2被中断
2执行finally
ReentranReadWriteLock分为readLock()和writeLock()来获取读写锁
public class ReadAndWriteLock {
private ReentrantReadWriteLock lock = new ReentrantReadWriteLock();
public static void main(String[] args) {
ReadAndWriteLock readAndWriteLock = new ReadAndWriteLock();
new Thread(() -> readAndWriteLock.read()).start();
new Thread(() -> readAndWriteLock.read()).start();
new Thread(() -> readAndWriteLock.write()).start();
new Thread(() -> readAndWriteLock.write()).start();
}
public void read() {
lock.readLock().lock();
try {
System.out.println(Thread.currentThread().getName() + "正在进行读操作");
long startTime = System.currentTimeMillis();
while ((System.currentTimeMillis() - startTime) < 10000){}
} catch (Exception e) {
} finally {
System.out.println(Thread.currentThread().getName() + "读操作完毕");
lock.readLock().unlock();
}
}
public void write() {
lock.writeLock().lock();
try {
System.out.println(Thread.currentThread().getName() + "正在进行写操作");
long startTime = System.currentTimeMillis();
while ((System.currentTimeMillis() - startTime) < 10000){}
} catch (Exception e) {
} finally {
System.out.println(Thread.currentThread().getName() + "写操作完毕");
lock.writeLock().unlock();
}
}
}
输出:
Thread-0正在进行读操作
Thread-1正在进行读操作
Thread-0读操作完毕
Thread-1读操作完毕
Thread-2正在进行写操作
Thread-2写操作完毕
Thread-3正在进行写操作
Thread-3写操作完毕
Lock和synchronized的选择:
1.Lock是JDK并发包的一个类,synchronized是关键字
2.Lock必须要使用unlock()方法在finally中释放,如果没有主动释放锁,就有可能导致出现死锁现象,发生异常时不会自动释放,synchronized发生异常时会自动释放
3.Lock在等待获取锁的时候可以响应中断,synchronized则不会。
4.Lock可以知道获取锁是否成功
5.Lock有读写锁功能
6.Lock可以在指定的时间范围内获取所,如果截止时间到了,依然没有获得锁,则返回