1.线程安全问题:
在多线程中,有可能出现多个线程同时使用同一个资源的情况,这个资源可以是变量,数据表,txt文件等。这个资源称作"临界资源"
举个例子:取钱这个线程分为两个步骤:
1.读取金额
2.取款
3.更新金额
有个典型的线程安全的例子,倘若A,B两人使用同一个账户(1000元)取款,A执行
1.读取金额 2.取款,取出300元,并未更新金额。
此时,
B读取金额,显示为1000(应该为700),B取出1000元。
最后,A执行3.更新金额,B执行3.更新金额。
如前面所说,多个线程访问临界变量,就会产生线程安全问题。不过,当多个线程执行同一个方法时,方法内部的变量不是临界资源,因为每个线程都有自己独立的内存区域(PC,方法栈,线程栈)
2.解决线程安全问题:
基本所有的并发方案,都采用“序列化访问资源”,也就是在同一时间,只有一个线程能访问临界资源,也称作同步互斥访问
方案1:synchronized
在Java中,每个对象都有一个锁标记,称为monitor(监视器),多个线程访问这个对象的临界资源时,只有获取了该对象的锁才能访问
在Java中,可以使用synchronized关键字来标记一个方法或者代码块,当某个线程调用该对象的synchronized方法或者访问synchronized代码块时,这个线程便获得了该对象的锁,其他线程暂时无法访问这个方法,只有等待这个方法执行完毕或者代码块执行完毕,这个线程才会释放该对象的锁,其他线程才能执行这个方法或者代码块。
public class ThreadLock2{
public static void main(String[] agrs) {
final 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 {
private List<Integer> list = new ArrayList<>();
//在这其实是锁住了InsertData对象的monitor锁
public synchronized void insert(Thread thread) {
for (int i = 0; i < 5; i++) {
System.out.println(thread.getName() + "插入: " + i);
list.add(i);
}
}
}
输出结果:
线程1插入: 0
线程1插入: 1
线程1插入: 2
线程1插入: 3
线程1插入: 4
线程2插入: 0
线程2插入: 1
线程2插入: 2
线程2插入: 3
线程2插入: 4
试着把 public synchronized void insert(Thread thread) 中的 synchronized去掉,再看看结果
此时,线程1和线程2可能会交叉运行,因为insert这个方法不再是临界资源了(允许多个线程同时进入)
insert方法还可以改成以下两种方式:
class InsertData extends Thread {
private List<Integer> list = new ArrayList<>();
public void insert(Thread thread) {
synchronized (this) {
for (int i = 0; i < 5; i++) {
System.out.println(thread.getName() + "插入: " + i);
list.add(i);
}
}
}
}
class InsertData extends Thread {
private List<Integer> list = new ArrayList<>();
private Object object = new Object();
public synchronized void insert(Thread thread) {
synchronized (object) {
for (int i = 0; i < 5; i++) {
System.out.println(thread.getName() + "插入: " + i);
list.add(i);
}
}
}
}
说明:
1)当一个线程正在访问一个对象的synchronized方法,那么其他线程不能访问该对象的其他synchronized方法。这个原因很简单,因为一个对象只有一把锁,当一个线程获取了该对象的锁之后,其他线程无法获取该对象的锁,所以无法访问该对象的其他synchronized方法。
2)当一个线程正在访问一个对象的synchronized方法,那么其他线程能访问该对象的非synchronized方法。这个原因很简单,访问非synchronized方法不需要获得该对象的锁,假如一个方法没用synchronized关键字修饰,说明它不会使用到临界资源,那么其他线程是可以访问这个方法的,
3)如果一个线程A需要访问对象object1的synchronized方法fun1,另外一个线程B需要访问对象object2的synchronized方法fun1,即使object1和object2是同一类型的对象,也不会产生线程安全问题,因为他们访问的是不同的对象,所以不存在互斥问题。
synchronized的另外一种使用方式:
public class MyThread{
private Object lock = new Object();
private ArrayList<Integer> list = new ArrayList<>();
public static void main(String[] args) {
MyThread mh = new MyThread();
ThreadTest tt = mh.new ThreadTest();
ThreadTest tt2 = mh.new ThreadTest();
tt.start();
tt2.start();
}
class ThreadTest extends Thread{
@Override
public void run() {
synchronized (lock) {
for(int i=0;i<5;i++){
list.add(i);
System.out.println(Thread.currentThread().getName()
+ "insert:" +i);
}
}
}
}
}
输出结果:
Thread-0insert:0
Thread-0insert:1
Thread-0insert:2
Thread-0insert:3
Thread-0insert:4
Thread-1insert:0
Thread-1insert:1
Thread-1insert:2
Thread-1insert:3
Thread-1insert:4
注意事项:
static方法,使用的是类锁(多个对象使用同一个类的monitor)
和非static方法,使用的是对象锁(每个对象维护一个monitor)
synchronized的缺陷:
1.使用synchronized包住的代码块,只可能有两种状态:顺利执行完毕释放锁,执行发生异常释放锁,不会由于异常导致出现死锁现象
2.如果synchronized包住的代码块中有sleep等操作,比如I/O阻塞,但是其他线程还是需要等待,这样程序的效率就比较低了
3.等待synchronized释放锁的线程会一直等待下去(死心塌地,不到黄河心不死)