
线程同步:当有一个线程在对内存进行操作时,其他线程都不可以对这个内存地址进行操作,直到该线程完成操作, 其他线程才能对该内存地址进行操作,而其他线程又处于等待状态,实现线程同步的方法有很多。
在一般情况下,创建一个线程是不能提高程序的执行效率的,所以要创建多个线程。
为什么要线程同步
多个线程同时运行的时候可能调用线程函数,在多个线程同时对同一个内存地址进行写入,由于CPU时间调度上的问题,写入数据会被多次的覆盖,所以就要使线程同步。
例如:我们去银行存钱,那肯定是我们银行卡里原本的钱加上要存入的钱。但是在你存钱的同时你的朋友在给你的银行卡转钱,这是两个线程,这两个线程同时拿到了银行卡的本金,那么这两个线程最后都会返回一个总金额,那这两个总金额都是不正确的,只有这两次交易有一个先后顺序才行,这就是线程同步的一个原因。
基本上所有解决线程安全问题的方式都是采用“序列化临界资源访问”的方式,即在同一时刻只有一个线程操作临界资源,操作完了才能让其他线程进行操作,也称作同步互斥访问。
在Java中一般采用synchronized和Lock来实现同步互斥访问。
首先我们先来了解一下互斥锁,
互斥锁:就是能达到互斥访问目的的锁。
如果对一个变量加上互斥锁,那么在同一时刻,该变量只能有一个线程能访问,即当一个线程访问临界资源时,其他线程只能等待。
在Java中,每一个对象都有一个锁标记(monitor),也被称为监视器,当多个线程访问对象时,只有获取了对象的锁才能访问。
在我们编写代码的时候,可以使用synchronized修饰对象的方法或者代码块,当某个线程访问这个对象synchronized方法或者代码块时,就获取到了这个对象的锁,这个时候其他对象是不能访问的,只能等待获取到锁的这个线程执行完该方法或者代码块之后,才能执行该对象的方法。
我们先写一个不加Synchronized的多线程代码,这段代码是创建两个线程,这段代码是创建两个线程分别输出五个数。
多次运行可以发现结果每次不一样,这就导致了不确定性。我们给他加上同步方法会发现一个输出完之后另一个才会输出,这就可以空值共享资源不能同时被两个线程得到。
package hello;public class Hello { public static void main(String[] args) throws Exception { MyThread myThread = new MyThread(); Thread thread = new Thread(myThread); Thread thread1 = new Thread(myThread); thread.start(); thread1.start(); }}class OutputData{ //定义输出方法 public void output(Thread thread){ for (int i=0;i<5;i++){ System.out.println(thread.getName()+":"+"输出"+i); } }}class MyThread implements Runnable{ OutputData inserData = new OutputData(); public void run(){ inserData.output(Thread.currentThread()); }}我们在OutputData类里面加入synchronized之后在运行就可以看到结果每次都是0123401234
class OutputData{ //定义输出方法 public synchronized void output(Thread thread){ for (int i=0;i<5;i++){ System.out.println(thread.getName()+":"+"输出"+i); } }}其实上面的代码还可以这样加,这个里面和上面的原理是一样的。
class OutputData{ //定义输出方法 public void output(Thread thread){ //this就是当前对象 synchronized (this) { for (int i = 0; i < 5; i++) { System.out.println(thread.getName() + ":" + "输出" + i); } } }}如果一个方法或者代码块被synchronized关键字修饰,当线程获取到该方法或代码块的锁,其他线程是不能继续访问该方法或代码块的。
而其他线程要能访问该方法或代码块,就必须要等待获取到锁的线程释放这个锁,而在这里释放锁只有两种情况:
上面我们已经说了synchronized锁释放的时机
但是可能会有一种情况,当一个线程获取到对象的锁,然后在执行过程中因为一些原因(等待IO,调用sleep方法)被阻塞了,这个时候锁还在被阻塞的线程手中,而其他线程这个时候除了等之外,没有任何办法,我们想一想这样子会有多影响程序的效率。因此就需要有一种机制可以不让等待的线程一直无期限地等待下去(比如只等待一定的时间或者能够响应中断),通过Lock就可以办到。
在比如,当多个线程操作同一个文件的时候,同时读写是会冲突的,同时写也是会冲突的,但是同时读是不会发生冲突的,而我们如果用synchronized来实现同步,就会出现一个问题:
如果多个线程都只是进行读操作,所以当一个线程在进行读操作时,其他线程只能等待无法进行读操作。
因此就需要一种机制来使得多个线程都只是进行读操作时,线程之间不会发生冲突,而通过Lock就可以办到。
总的来说Lock要比synchronized提供的功能更多,可定制化的程度也更高,Lock不是Java语言内置的,而是一个类。
先看一下Lock接口的方法
首先lock()方法是平常使用得最多的一个方法,就是用来获取锁。如果锁已被其他线程获取,则进行等待。
由于在前面讲到如果采用Lock,必须主动去释放锁,并且在发生异常时,不会自动释放锁。因此一般来说,使用Lock必须在try{}catch{}块中进行,并且将释放锁的操作放在finally块中进行,以保证锁一定被被释放,防止死锁的发生。
一般格式:
Lock lock = ...; lock.lock(); try { //处理任务 }catch (Exception e){ //捕捉异常 }finally{ lock.unlock(); }package hello;import javax.sound.sampled.FloatControl;import java.util.concurrent.locks.Lock;import java.util.concurrent.locks.ReentrantLock;public class Hello { public static void main(String[] args) { Lock lock = new ReentrantLock(); OutputData outputData = new OutputData(); new Thread(){ public void run(){ outputData.output(Thread.currentThread(),lock); }; }.start(); new Thread(){ public void run(){ outputData.output(Thread.currentThread(),lock); }; }.start(); }}class OutputData{ public void output(Thread thread,Lock lock){ lock.lock(); try { System.out.println(thread.getName()+"得到了锁"); Thread.sleep(100);//加这个睡眠是为了结果效果明显 }catch (Exception e){ e.printStackTrace(); }finally { System.out.println(thread.getName()+"释放了锁"); lock.unlock(); } }}把OutPutData类里面的output方法修改一下就可以了
class OutputData{ public void output(Thread thread,Lock lock){ if(lock.tryLock()){ try { System.out.println(thread.getName()+"得到了锁"); Thread.sleep(100);//加这个睡眠是为了结果效果明显 }catch (Exception e){ e.printStackTrace(); }finally { System.out.println(thread.getName()+"释放了锁"); lock.unlock(); } }else{ System.out.println(thread.getName()+"获取锁失败"); } }}volatile含义和特点
我们知道每个线程运行的时候都有自己的工作内存,会把变量拷贝到自己的缓存中去,一般情况下你在自己缓存修改的变量不会立即重新写入主内存,这就导致类多线程同步问题,那么volatile关键字的特点是:
volatile关键字会产生什么效果
这段代码可以看出我们并没给有加锁,但是这个int变量sum还是按顺序加的,说明他在改变之后立即就把主内存里的变量改变了。也算是一种同步方式吧。
package hello;public class Hello { public static void main(String[] args) { AddClass addClass = new AddClass(); new Thread() { public void run(){ for (int j=0;j<100;j++){ addClass.add(); System.out.println(Thread.currentThread().getName()+":"+addClass.sum); } } }.start(); new Thread(){ public void run(){ for (int j=0;j<100;j++){ addClass.add(); System.out.println(Thread.currentThread().getName()+":"+addClass.sum); } } }.start(); }}class AddClass{ public volatile int sum = 0; public void add(){ sum++; }}火车站买票
package hello;public class Hello { public static void main(String[] args) { //实例化站台对象, Station station1=new Station(); Station station2=new Station(); Station station3=new Station(); // 让每一个站台对象各自开始工作 station1.start(); station2.start(); station3.start(); } }class Station extends Thread { static volatile int p = 20; static Object ob = new Object(); public void run() { while (p > 0) { synchronized (ob) { if (p > 0) { System.out.println("卖出了第" + p + "张票"); p = p - 1; } } synchronized (this) { if (p == 0) { System.out.println("票卖完了"); } } try { Thread.sleep(100); } catch (Exception e) { } } }}