线程的同步与安全

举报
小高先生 发表于 2022/05/27 22:11:40 2022/05/27
【摘要】 本文讲述了有关线程同步和安全的问题

  这篇文章总结了多线程实战第三章线程同步和安全这一章的知识点。本章主要讲述两点内容,分别是线程的同步和线程的安全

一.线程的同步安全

  设计并发线程的好处是使程序执行效率更高,但是会有安全问题,会出现数据一致性(数据精确)问题。简单来说就是并发线程能共享资源,一个资源(变量、对象、文件、数据库)能够被多个线程使用,就会出现问题,这样的资源成为共享资源临界区

  线程不安全案例 

public class SafeTest implements Runnable{
	
	int m = 0;
	public static void main(String[] args) throws InterruptedException {
		// TODO Auto-generated method stub
		SafeTest st = new SafeTest();
		Thread t1 = new Thread(st);
		Thread t2 = new Thread(st);
		t1.start();
		t2.start();
		
		//等两个线程执行结束后再执行main线程
		t1.join();
		t2.join();
		
		System.out.println(st.m);
	}

	@Override
	public void run() {
		// TODO Auto-generated method stub
		for(int i = 0;i<=10000;i++) {
			m++;
		}
	}

}

  预想的输出结果是20000,因为创建了两个线程,每个加到10000就应该是20000,但输出结果不是,这是为什么?假设线程1先执行m++,但是m++之后的结果还没来得及存储到内存中,线程2就开始执行m++,这样就导致两个线程只执行了一次m++。

  这就是不安全的程序,所以要设置安全操作。这时候就可以用到互斥访问之synchronized。

  什么是互斥锁,就是互斥访问目的的锁,如果临界资源(就是并发线程都可以访问的资源)加上互斥锁,当一个线程访问该临界资源时,其他线程就不能访问该资源,只能等待。在JAVA中,每一个对象都有一个锁标记,也被称为监视器,多线程访问某个对象时,只有拥有该对象锁的线程才能访问该对象。

  更改之后的程序

public class SafeTest implements Runnable{
	
	int m = 0;
	public static void main(String[] args) throws InterruptedException {
		// TODO Auto-generated method stub
		SafeTest st = new SafeTest();
		Thread t1 = new Thread(st);
		Thread t2 = new Thread(st);
		t1.start();
		t2.start();
		
		//等两个线程执行结束后再执行main线程
		t1.join();
		t2.join();
		
		System.out.println(st.m);
	}

	@Override
	public synchronized void run() {
		// TODO Auto-generated method stub
		for(int i = 0;i<10000;i++) {
			m++;
		}
	}

}

  要注意一个问题,锁安全的前提是多个线程持有同一个线程对象。下面这个案例设置了两个线程,两个线程对应两个对象,结果就是发生数据错误

public class SafeTest implements Runnable{
	
	private static int m = 0;
	public static void main(String[] args) throws InterruptedException {
		// TODO Auto-generated method stub
		SafeTest st1 = new SafeTest();
		SafeTest st2 = new SafeTest();
		Thread t1 = new Thread(st1);
		Thread t2 = new Thread(st2);
		t1.start();
		t2.start();
		
		//等两个线程执行结束后再执行main线程
		t1.join();
		t2.join();
		
		System.out.println(m);
	}

	@Override
	public synchronized void run() {
		// TODO Auto-generated method stub
		for(int i = 0;i<10000;i++) {
			m++;
		}
	}

}

二、线程的同步方法和同步块

  同步方法在上一节已经说过了,在方法前面加上synchronized,就可以给方法上锁,只能有一个线程访问

  这部分说一下与同步方法类似的同步块,引入同步块的目的和同步方法一样,都是保证资源能够被正确访问,不会出现数据错误,在同一时刻只能有一个线程访问。

  代码块如下:

  synchronized(obj){

            同步代码块;

  }

  obj叫做同步监视器(即锁对象),任何线程访问同步代码块之前必须要获得obj的锁,其他线程无法获得锁

  锁对象可以使任意对象,但必须保证同一对象任意时刻只能有一个线程可以获得同步监视器的锁,当同步代码块执行结束后,线程就会释放锁,其他线程才会得到锁

  案例:

public class SafeTest2 implements Runnable{
	
	public static int m = 0;
	@Override
	public void run() {
		synchronized(this) {
			for(int i=0;i<10000;i++ ) {
				m++;
			}
		}
	}

	public static void main(String[] args) throws InterruptedException {
		// TODO Auto-generated method stub
		
		SafeTest2 st = new SafeTest2();
		
		Thread t1 = new Thread(st);
		Thread t2 = new Thread(st);
		
		t1.start();
		t2.start();
		
		t1.join();
		t2.join();
		
		System.out.println(m);
	}

}

  同步方法和同步代码块还是有一点区别,区别在意同步方法上锁范围更大,整个方法都被锁上了,而代码块上锁范围能小一点,可以是方法里的一段代码

三、线程的死锁

  死锁,顾名思义,被锁死了,也就是这个线程永远的持有一个锁,其他线程向执行任务但是没有办法解锁,所以其他线程都处于阻塞状态。举个例子就是如果线程A持有锁L并且想获得锁M,另一个线程B持有锁M并向获得锁L,于是这两个线程在不停的等待,因此两个线程都处于阻塞状态,这就是简单的死锁。

  案例:

public class DeadLock {
	private final Object left = new Object();
	private final Object right = new Object();
	
	public void leftRight() throws InterruptedException {
		synchronized(left) {
			//一定要令线程睡眠,不然很可能另一个线程没执行呢,这个线程已经把right和left都上锁了
			Thread.sleep(2000);
			synchronized(right) {
				System.out.println("leftRight");
			}
		}
	}
	
	public void rightLeft() throws InterruptedException {
		synchronized(right) {
			Thread.sleep(2000);
			synchronized(left) {
				System.out.println("rightLeft");
			}
		}
	}
}

public class Thread0 extends Thread{
	private DeadLock dl;
	
	public Thread0(DeadLock dl) {
		this.dl = dl;
	}
	@Override
	public void run() {
		try {
			dl.leftRight();
		} catch (InterruptedException e) {
			// TODO Auto-generated catch block
			e.printStackTrace();
		}
	}
	
}

public class Thread1 extends Thread{
	private DeadLock dl;
	
	 public Thread1(DeadLock dl) {
		 this.dl = dl;
	 }

	@Override
	public void run() {
		try {
			dl.rightLeft();
		} catch (InterruptedException e) {
			// TODO Auto-generated catch block
			e.printStackTrace();
		}
	}
	 
	 
}

public class DeadLockTest {

	public static void main(String[] args) {
		// TODO Auto-generated method stub
		DeadLock dl = new DeadLock();
		Thread0 t0 = new Thread0(dl);
		Thread1 t1 = new Thread1(dl);
		
		t0.start();
		t1.start();
	}

}

四.线程的明锁

  锁对象Lock可以方便实现资源封锁,用来对竞争资源并发访问的控制

  Lock.lock():获取锁

  Lock.unlock():释放锁

  Lock锁可以创建公平锁和非公平锁,默认是非公平锁

import java.util.concurrent.locks.Lock;
import java.util.concurrent.locks.ReentrantLock;

public class Count {
	private int num;
	private Lock lock = new ReentrantLock();
	
	public Count(int num) {
		this.num = num;
	}
	
	public void operator(int OperatorNum) {
		lock.lock();
		this.num+=OperatorNum;
		System.out.println(Thread.currentThread().getName()+",操作的数是"+OperatorNum+",操作之后的数是"+this.num);
		lock.unlock();
	}
}

public class ThreadOperator extends Thread{
	private Count c;
	private int OperatorNum;
	
	public ThreadOperator(Count c,int OperatorNum,String ThreadName) {
		super(ThreadName);//父类
		this.c = c;
		this.OperatorNum = OperatorNum;
	}
	@Override
	public void run() {
		c.operator(OperatorNum);
	}
	
}

public class TestOperatorTest {

	public static void main(String[] args) {
		// TODO Auto-generated method stub
		Count c = new Count(10);
		ThreadOperator t1 = new ThreadOperator(c,20,"线程A");
		ThreadOperator t2 = new ThreadOperator(c,10,"线程B");
		ThreadOperator t3 = new ThreadOperator(c,-20,"线程C");
		
		t1.start();
		t2.start();
		t3.start();
	}

}

  期待的输出结果就是20,确实,实际输出结果确实是20,所以锁起作用了。

  根据输出结果可以看出来,一个线程执行的时候,其他线程无法执行

  那试试不加锁呢,会是什么样,结果如下:

  这就能看出来,出现了数据错误,所以要加锁

  学习完Lock对象,可以看出来Lock和synchronized的作用是一样的,都可以给对象上锁,使多线程中只能有一个线程执行任务。那它们之间有什么区别呢?区别如下:

  •   Lock不是JAVA内置,需要创建,而synchronized是JAVA内置的关键字,Lock是一个类
  •   synchronized不需要手动释放锁,系统自动释放。Lock需要手动解锁,不手动解锁就可能出现死锁

五.线程的公平锁和非公平锁

  在创建Lock类的时候其实已经用到了,只是没具体讲,ReenTrantLock就是用来实现公平锁和非公平锁的。那什么是公平锁,什么又是非公平锁呢?

  公平锁就是,当一个线程具有锁的时候,其他线程肯定无法获得锁,那它们就会等锁,等不是乱等的,而是有顺序,这个顺序就是它们发出请求的顺序,比如线程1在执行任务,线程2在等待,之后是线程3发出请求等待,等线程1释放锁后,是线程2先获得锁,然后才是线程3.

  非公平锁和公平锁就差在公平上,非公平锁被一个线程占据后,其他排队的线程是乱排的,谁都有机会去抢锁。

  看一下这两个锁的案例

  非公平

  

import java.util.concurrent.locks.Lock;
import java.util.concurrent.locks.ReentrantLock;

public class UserRunn implements Runnable{
	private int count = 10;//车票
	private boolean flag = true;//线程退出标志位
	
	Lock lock = new ReentrantLock(true);

	@Override
	public void run() {
		// TODO Auto-generated method stub
		
		System.out.println(Thread.currentThread().getName()+":欢迎购票");
		while(flag) {
			lock.lock();
			if (count>0) {
				System.out.println(Thread.currentThread().getName()+":获取第"+(10-(count--)+1)+"张票");
				try {
					Thread.sleep(200);
				} catch (InterruptedException e) {
					// TODO Auto-generated catch block
					e.printStackTrace();
				}
			}else {
				flag = false;
			}
			lock.unlock();
		}
		
		System.out.println(Thread.currentThread().getName()+":谢谢您");
		
	}
	
	
}

public class UserRunnTest {

	public static void main(String[] args) {
		// TODO Auto-generated method stub
		UserRunn us = new UserRunn();
		Thread t1 = new Thread(us,"北门汽车站");
		Thread t2 = new Thread(us,"南门汽车站");
		Thread t3 = new Thread(us,"东门汽车站");
		
		t1.start();
		t2.start();
		t3.start();
	}

}

  结果可以看出是有顺序的,按照申请顺序来,先申请的先退出,然后它排在申请队列的末尾

  非公平锁结果就是毫无顺序可言,可能一个线程一直能抢到

  不上锁指定不对,就会出现下面的情况,发生了数据错误

六.总结

  本章主要讲了线程同步和安全的问题,通过同步代码块和上锁的方式,保证数据安全

  

  

【版权声明】本文为华为云社区用户原创内容,转载时必须标注文章的来源(华为云社区)、文章链接、文章作者等基本信息, 否则作者和本社区有权追究责任。如果您发现本社区中有涉嫌抄袭的内容,欢迎发送邮件进行举报,并提供相关证据,一经查实,本社区将立刻删除涉嫌侵权内容,举报邮箱: cloudbbs@huaweicloud.com
  • 点赞
  • 收藏
  • 关注作者

评论(0

0/1000
抱歉,系统识别当前为高风险访问,暂不支持该操作

全部回复

上滑加载中

设置昵称

在此一键设置昵称,即可参与社区互动!

*长度不超过10个汉字或20个英文字符,设置后3个月内不可修改。

*长度不超过10个汉字或20个英文字符,设置后3个月内不可修改。