最近在看面经,关于Java的面试,老生常谈的一个问提就是多线程的安全问题和线程的同步,在此我不免要从它的概念开始讲述。
一、什么是非线程安全
我相信,浏览这篇博客的读者都知道什么是进程,什么是线程,进程与线程的区别。(如果你还不知道,那么可以去查阅一下。)其中重要的一点区别就是:进程拥有独立的地址空间,拥有属于自己的在资源,而线程不拥有资源,它共享着进程的资源。所以当一个进程中的多线程并发运行时,就容易造成数据的不一致和数据污染问题。比如:
小王在某银行存了100元,然后它要取50元,这时进程A查询信息剩余金钱100元,可以取钱,此时进程A修改金钱100-50,然后小王又要取100,如果进程B优先进程A获取剩余金钱100,那么它可以继续取钱,人们设想的是进程B应该在数据更改之后才能执行,但是线程是并发执行的,我们若不规定它的执行顺序,那么银行就会亏损。
二、判断线程是否安全的标准
下面这个函数,是线程安全的吗?
显然,是安全的,不管在什么情况下,最后的执行结果都是count=1。我们看到这段代码没有任何状态,它没有任何作用域,也没有任何引用,执行的作用范围只存在它这条现成的局部变量中,不对其他线程产生影响,也就是说多线程间没有共享数据,彼此间的行为相对独立互不影响,那么线程就是安全的
public void test() {
int count = 0;
count = count+1;
System.out.println("count = "+count);
}
相反如果我们给这段代码添加状态,我们将count设为全局变量,如果单线程运行结果是唯一的,如果多线程运行结果就不唯一了。
static int count = 0;//全局变量
public void test() {
count = count++;
System.out.println("count = "+count);
}
多线程运行结果显示如下,可以发现于我们设想的结果完全不一样。
综上,我们可以发现,判断一个程序是否存在线程安全问题的标准在于
1.是否是多线程环境
2.是否存在共享数据
3.是否有多条语句操作共享数据
三、实现线程安全的方式
针对线程不安全的存在条件,在Java中有两种解决思路
1.让共享资源至多被一个线程占用——同步
2.让线程拥有独立资源,不去共享资源
四、同步的实现方式
1.synchronized(互斥锁)
synchronized关键字用来控制线程同步,保证在多线程环境下,保证共享数据的一致性,它既可以修饰方法也可以修饰代码块,但修饰方法更加常用。
1.修饰方法:
实现原理:synchronized相当于一把锁,用来锁住对象,其他线程只有等当前线程执行完数据释放锁对象才能使用,否则一直处于等待状态。
注:
- 非静态同步方法:锁对象默认是this
- 静态同步方法:锁对象默认是:类名.class(每个类都有一个class对象,而且是唯一的)
实现方法
修饰符 synchronized 返回值类型 方法名(参数列表){
}
举例说明:
public class MyTest implements Runnable{
static int count=0;
public synchronized void test() {
count++;
System.out.println("count="+count);
}
public void run() {
test();
}
public static void main(String[] args) {
MyTest mt = new MyTest();
for(int i=0;i<10;i++) {
Thread t = new Thread(mt);
t.start();
}
}
}
运行结果显示线程安全了!
synchronized在使用时有一个经常忽略的问题:它锁的是括号内的对象而不是代码,如果多线程处理的不是一个对象,那么synchronized锁就不起作用了,我们将上面的代码main函数修改成如下所示,每次循环产生一个新的MyTest对象,这样运行结果出现了错误。
for(int i=0;i<10;i++) {
MyTest mt = new MyTest();
Thread t = new Thread(mt);
t.start();
}
2.修饰代码块
实现原理:
保证同一时间只有一个线程执行代码块中的代码。
实现方法:
synchronized(锁){
// 操作共享资源的代码
}
注:括号中的锁可以是任何对象
举例说明:修改上述代码
//创建锁对象
private static Object lock = new Object();
public void test() {
synchronized(lock){
count++;
System.out.println("count="+count);
}
}
运行结果如下:
2.Lock(可重入锁)
synchronized虽然实现简单,但是容易浪费资源,假设一个线程获取了对应的锁并执行,那么其他线程需要用到该资源只能无穷等待,只有当该线程释放锁。释放锁有两种情况:
1.获取锁的线程执行结束,自动释放锁
2.获取锁的线程执行发生异常,JVM让线程放锁
因此在JDK1.5中java为我们提供了Lock,实现synchronized相同的功能,并且让锁有了可操作性,可以手动的获取和释放锁,甚至还可以中断获取以及超时获取的同步特性,但是从使用上说Lock明显没有synchronized使用起来方便快捷。
Lock和synchronized的区别:
1.Lock是一个接口,基于JDK层面实现,而synchronized是Java中的关键字,synchronized是Java内置的语言,基于JVM实现。
2.synchronized不需要手动释放锁,当synchronized方法或者synchronized代码块执行完之后,系统会自动让线程释放对锁的占用;而Lock则必须要用户去手动释放锁,如果没有主动释放锁,就有可能导致出现死锁现象。
Lock用于获取锁,但它不会主动释放锁所以需要与unlock()配合使用。一般在使用Lock时必须在try{}catch{}块中进行,并且将释放锁的操作放在finally块中进行,以保证锁一定被被释放,防止死锁的发生。
在这里我推荐大家去看博客可以说写的很详细了!https://blog.csdn.net/csdnnews/article/details/82321777#commentBox