目录
引子
private static int cs=0;//全局临界资源
public static void main(String[] args) throws InterruptedException {
Thread t1 = new Thread(() -> {
for (int i = 0; i < 5000; i++) {
cs++;
}
});
Thread t2 = new Thread(() -> {
for (int i = 0; i < 5000; i++) {
cs--;
}
});
t1.start();
t2.start();
t1.join();
t2.join();
log.debug("临界资源为:{}",cs); //得到的结果可能会不对,有时并不是0
问题分析:
以上的结果可能是正数、负数、零。为什么呢?因为Java 中对变量的自增,自减并不是原子操作,必须从字节码来进行分析,++运算和--运算是先取出变量的值、运算得出结果、放进变量里,这三步操作才组成了++和--运算。
如上图:有时候线程2已经运算完了,但是还未把结果反馈回去,此时cpu的时间片用完了,被线程1抢走了,然后线程1拿到了脏数据,进行运算并反馈了结果,然后又轮到线程2继续执行, 把结果再反馈回去,这时候结果就是错误的了。
临界区cs
- 在多个线程读共享资源没问题,当多个线程对共享资源读写操作时发生指令交错,就会出现线程安全问题
- 一段代码块内如果存在对共享资源的多线程读写操作,称这段代码块为临界区
- 为了避免临界区的竞态条件发生,有多种手段可以达到目的。
解决方案:
- 阻塞式的解决方案: synchronized,Lock
- 非阻塞式的解决方案:原子变量
synchronized
synchronized,来解决上述问题,即俗称的【对象锁】,它采用互斥的方式让同一时刻至多只有一个线程能持有【对象锁】,其它线程再想获取这个【对象锁】时就会阻塞住。保证拥有锁的线程可以安全的执行临界区内的代码,不用担心线程上下文切换
注意:
虽然java中互斥和同步都可以采用synchronized关键字来完成,但它们还是有区别的:
- 互斥是保证临界区的竞态条件发生,同一时刻只能有一个线程执行临界区代码
- 同步是由于线程执行的先后、顺序不同、需要一个线程等待其它线程运行到某个点
private static Object lock=new Object();
//加同步代码块
synchronized (lock) {
cs++;//临界区
}
解读:多个线程同时争夺这把锁,谁抢到了谁就能执行临界区里的代码,没抢到的线程会进入阻塞状态,抢到锁的线程如果执行完了临界区的代码才会释放锁,并唤醒因为没抢到锁而阻塞的线程,让他们继续抢锁。
synchronized实际是用对象锁保证了临界区内代码的原子性,临界区内的代码对外是不可分割的不会被线程切换所打断。
synchronized代码块中指令是可以重排序的,只不过代码块之外是看不到它的重排序的。
语法
1.放在方法内
synchronized(对象)
{
临界区(对象需要受保护的代码)
}
2.放在方法上
class Test{
public synchronized void test() {
临界区
}
}
---等价于---
class Test{
public void test() {
synchronized(this){ //锁本类非静态对象
临界区
}
}
3.放在静态方法上
class Test{
public synchronized static void test() {
临界区
}
}
---等价于---
class Test{
public void test() {
//锁的是类对象
synchronized(Test.class){ //静态对象不等于非静态对象,非静态对象存储在堆,静态的存储在方法区(是所有本类对象共享的)
临界区
}
}
线程八锁
就是对synchronized语法的8个练习题,我们来看看吧!分析出synchronized到底锁的是哪个对象
情况一:
public static void main(String[] args) throws ExecutionException, InterruptedException, TimeoutException {
Number n = new Number();
new Thread(()->{
n.a();
}).start();
new Thread(()->{
n.b();
}).start();
}
static class Number{
public synchronized void a(){
System.out.print("a");
}
public synchronized void b(){
System.out.print("b");
}
}
答案为 ab 或者 ba,这两个线程锁住的都是n这个变量。
情况二:更情况一的不同之处就是在a方法里多了一个sleep
static class Number{
public synchronized void a() {
try {
Thread.sleep(1000);
} catch (InterruptedException e) {
e.printStackTrace();
}
System.out.print("a");
}
public synchronized void b(){
System.out.print("b");
}
}
答案:如果第一个线程先抢到了锁那么,就会隔1s,再输出ab,如果第二个线程先抢到锁,那么先输出b再隔一秒输出a。这道题的特点就是告诉我们抢到锁的线程,如果睡眠了是不会释放锁的。
情况三:多加了一个c方法,该方法是没有加synchronized的。
public static void main(String[] args) throws ExecutionException, InterruptedException, TimeoutException {
Number n = new Number();
new Thread(()->{
n.a();
}).start();
new Thread(()->{
n.b();
}).start();
new Thread(()->{
n.c();
}).start();
}
static class Number{
public synchronized void a() {
try {
Thread.sleep(1000);
} catch (InterruptedException e) {
e.printStackTrace();
}
System.out.print("a");
}
public synchronized void b(){
System.out.print("b");
}
public void c(){
System.out.print("c");
}
}
结果:c 1s ab 或bc 1s a或cb 1s a,这个c是不受控制的,因为他不需要争抢锁,所以线程开启后抢到cpu时间片后就能立即执行。
情况四:线程一和线程二执行的是不同的对象
public static void main(String[] args) throws ExecutionException, InterruptedException, TimeoutException {
Number n1 = new Number();
Number n2 = new Number();
new Thread(()->{
n1.a();
}).start();
new Thread(()->{
n2.b();
}).start();
}
static class Number{
public synchronized void a() {
try {
Thread.sleep(1000);
} catch (InterruptedException e) {
e.printStackTrace();
}
System.out.print("a");
}
public synchronized void b(){
System.out.print("b");
}
}
结果为:b 1s后 a,因为成员方法上的synchronized锁的是this,所以a锁的是n1,b锁的是n2,所以都能立即得到锁。
情况五:方法a变为static的方法
public static void main(String[] args) throws ExecutionException, InterruptedException, TimeoutException {
Number n = new Number();
new Thread(()->{
n.a();
}).start();
new Thread(()->{
n.b();
}).start();
}
static class Number{
public static synchronized void a() {
try {
Thread.sleep(1000);
} catch (InterruptedException e) {
e.printStackTrace();
}
System.out.print("a");
}
public synchronized void b(){
System.out.print("b");
}
}
答案:同情况四,因为a方法锁的是类对象,所以也不是同一把锁。
情况六:方法b也变为static的方法。
答案:1s后ab或者 b 1s后a,因为是同一把锁,互斥访问。
情况七:
public static void main(String[] args) throws ExecutionException, InterruptedException, TimeoutException {
Number n1 = new Number();
Number n2 = new Number();
new Thread(()->{
n1.a();
}).start();
new Thread(()->{
n2.b();
}).start();
}
static class Number{
public static synchronized void a() {
try {
Thread.sleep(1000);
} catch (InterruptedException e) {
e.printStackTrace();
}
System.out.print("a");
}
public synchronized void b(){
System.out.print("b");
}
}
答案:也是锁的不是一个对象,所以是b1s后a
情况八:在情况七的方法b上加上static
答案:锁的是同一个对象,互斥访问。
变量的线程安全
线程三大问题:原子性,可见性,有序性
线程安全问题=共享数据+多线程+多线程修改共享数据
局部变量是否线程安全?
- 局部变量是线程安全的
- 但局部变量引用的对象则未必
-
如果该对象没有逃离方法的作用访问,它是线程安全的
-
如果逃离了需要考虑线程安全
-
把局部变量引用的对象暴露给外部,可能就会不安全
int,float,double..这些属于基本数据类型,可以直接存放在栈帧的 局部变量中。而其他类对象,在局部变量表存放的是引用,实例在堆中
全局变量不安全例子:
俩线程同时写,只写成功一个,删除却要进行俩次
public class ThreadUnSafe {
ArrayList<String> list = new ArrayList<>();//全局变量不安全。
public void method1(){
for (int i=0;i<200;i++) {
method2(list); //临界区,可以在这里加锁
method3(list);
}
}
private void method2(ArrayList<String> list) {
list.add("1");
}
private void method3( ArrayList<String> list) {
list.remove(0);
}
}
ThreadUnSafe t1 = new ThreadUnSafe();
for (int i=0;i<200;i++) {
new Thread(() -> {
t1.method1();//同时引用同一个对象,共享list集合 所以线程不安全,可能同时add的时候只加了一个
}).start();
}
局部变量不安全的例子:
如果把method2开一个新线程执行add操作,则存在线程安全性问题,因为虽然是局部变量,但是它的引用暴露给了外部。
public void method1(){
ArrayList<String> list = new ArrayList<>();
for (int i=0;i<200;i++) {
method2(list);
method3(list);
}
}
private void method2(ArrayList<String> list) {
new Thread(()->{ //此时可能出现list集合被同时加和减的操作,会导致线程不安全
list.add("1");
}).start();
}
private void method3( ArrayList<String> list) {
list.remove(0);
}
常见线程安全类
- String
- Integer,Boolean等包装类 没有synchronized但是被final 修饰了
- StringBuffer
- Random
- Vector
- HashTable
- java.util.concurrent(JUC)包下的类
这里说它们是线程安全的是指,多个线程调用它们同一个实例的某个方法时,是线程安全的。也可以理解为
- 它们的单个方法是原子的,因为在方法上加了synchronized
- 但注意它们多个方法的组合不是原子的
线程安全类方法的组合
如:不是安全的,key的值可能就被覆盖了
Hashtable hashtable = new Hashtable();
//线程一、线程二
if (hashtable.get("key") == null){
hashtable.put("key",value);
}
有锁会阻塞,但是线程2是在线程1执行完get时候,立马又去执行get的。get是一把锁,put是一把锁,get完后将锁释放线程2可以继续get
线程1执行完get之后,锁释放,此时有可能线程2抢占到了锁,所以线程2也判断get==null成立,这时线程2可以进入下面的逻辑。这样和我们希望的流程就有可能不同了。
不可变类线程安全性
String,Integer等都是不可变类,内部的状态不可以改变,因此它们的方法都是线程安全的、
或许有疑问,String有replace,substring等方法【可以】改变值啊,那么这些方法又是如何保证线程安全的呢?
public String substring(int beginIndex) {
if (beginIndex < 0) {
throw new StringIndexOutOfBoundsException(beginIndex);
}
int subLen = value.length - beginIndex;
if (subLen < 0) {
throw new StringIndexOutOfBoundsException(subLen);
} //创建了一个新的对象
return (beginIndex == 0) ? this : new String(value, beginIndex, subLen);
}
卖票问题
出线程安全性的代码如下:
public class TicketWindow {
private int count;
public int sell(int amount){
//临界区
if(this.count>=amount){
this.count-=amount;//这里出问题,多个线程同时操作
return amount;
}else return 0;
}
public TicketWindow(int count) {
this.count = count;
}
}
public static void main(String[] args) throws InterruptedException {
TicketWindow window=new TicketWindow(10000);
Vector<Integer> v=new Vector();//需要线程安全的集合
List<Thread> threads=new ArrayList<>();//因为这个变量只是在main线程里
for(int i=0;i<2000;i++){
Thread thread = new Thread(() -> {
int a = window.sell(2);//线程不安全
v.add(a);//线程安全的,因为是Vector,如果是其他的则线程不安全,因为方法里多少会有读写的一些操作导致数据错误
});
threads.add(thread);
thread.start();
}
//等待所有线程
for (Thread thread : threads) {
thread.join();
}
//卖出去的票和
System.out.println(v.stream().mapToInt(i -> i).sum());//4000
System.out.println(window.getCount());//6004
}
解决方法:在sell方法上加synchronized,保证临界区的原子性。
转账问题
public class Account {
private int money;
//转账操作
public void transfer(Account target,int amount){
if(this.money>=amount){
this.setMoney(getMoney()-amount);
target.setMoney(target.getMoney()+amount);
}
}
}
public static void main(String[] args) throws InterruptedException {
Account a = new Account(2000);
Account b = new Account(1000);
Thread t1 = new Thread(() -> {
for (int i = 0; i < 400; i++) {
a.transfer(b, 2);
}
});
Thread t2 = new Thread(() -> {
for (int i = 0; i < 400; i++) {
b.transfer(a, 3);
}
});
t1.start(); t2.start();
t2.join(); t1.join();
System.out.println(a.getMoney()+" "+b.getMoney());//数目不对
}
如果只是在transfer方法上加synchronized的话,还是会出现问题。
因为synchronized只是锁this,而方法里涉及了两个不同的对象,同时只能a对象转账给b对象一笔账,同时只能b转a一笔,但是可能在同步代码块中,可能b要在读取自身money时,可能a恰巧想改变b的money,但是a的cpu时间片没了,b却读取了一个错误的值进行操作,操作完后,下一次a重新运行,又把上次没修改的值给修改了。
其实这里锁的是this的话,加了和没加一样,因为锁的是两个不同的对象,必须锁住两个线程唯一的对象,可以在方法上加static或者锁类,效率稍差。
下一篇synchronized的原理------------>synchronized原理