Java实现线程同步的五种方法

一.使用synchronized关键字
由于每个java对象都有一个内置锁,用synchronized修饰方法或者代码块时,内置锁会保护整个方法或代码块,要想执行这个方法或者代码块必须获得其内置锁,运行时会加上内置锁,当运行结束时,内置锁会打开。由于同步是一种高开销的工作,所以尽量减少同步的内容,只需同步代码块就可以。
1.修饰方法

public class Test implements Runnable {
    static int i=0;
    public synchronized void test(){
    	System.out.println("当前线程为"+i);
    	i++;
    }
    public void run(){
    	test();
    }
	public static void main(String[] args) {
		// TODO Auto-generated method stub
        Test t1=new Test();
        for(int i=0;i<10;i++){
        	Thread t11=new Thread(t1);
            t11.start();
        }
        
	}

}

结果如下:
在这里插入图片描述
从上面结果看出,使用synchroniz修饰方法会在每个线程中按顺序依次执行。
在使用该方法时要注意以下一点
先看代码如下:

public class Test implements Runnable {
    static int i=0;
    public synchronized void test(){
    	System.out.println("当前线程为"+i);
    	i++;
    }
    public void run(){
    	test();
    }
	public static void main(String[] args) {
		// TODO Auto-generated method stub
        for(int i=0;i<10;i++){
        	Test t1=new Test();
        	Thread t11=new Thread(t1);
            t11.start();
        }
        
	}

}

在看下结果:
在这里插入图片描述
只是稍微修改了一处,结果就有很大不同,为什么会这样呢?我们发现上面两个代码,一个是在开始值创建一个Runnable的对象,一个是在for循环中每次都创建一个新的对象,就是因为synchronized是不能锁住不同对象的线程的,只能锁住同一个对象的线程,也就是说锁住的是方法所属的主体对象自身。

2.修饰代码块

public class Test implements Runnable {
    static int i=0;
    public void test(){
    	synchronized(this){System.out.println("当前线程为"+i);
    	i++;
    	}
    }
    public void run(){
    	test();
    }
	public static void main(String[] args) {
		// TODO Auto-generated method stub
		Test t1=new Test();
        for(int i=0;i<10;i++){
        	Thread t11=new Thread(t1);
            t11.start();
        }
        
	}

}

运行结果和第一个程序一样的,不多解释了。

3.修饰静态方法

public static synchronized void anotherMethod() { 
    // do something 
} 

对于静态方法,锁住的不是这个类的对象,也不是也不是这个类自身,而是这个类所属的java.lang.Class类型的对象

二.wait与notify

  • wait(),使一个线程处于等待状态,并释放所持对象的锁,与sleep不同,sleep不会释放对象锁。
  • notify(),唤醒一个处于阻塞状态的线程,进入就绪态,并加锁,只能唤醒一个线程,但不能确切知道唤醒哪一个,由JVM决定,不是按优先级。其实不是对对象锁的唤醒,是告诉调用wait方法的线程可以去竞争对象锁了。wait和notify必须在synchronized代码块中调用。
  • notifyAll(),唤醒所有处于阻塞状态的线程,并不是给他们加锁,而是让他们处于竞争。
    为什么wait和notify要在synchronized代码块中使用
    调用wait()就是释放锁,释放锁的前提是必须要先获得锁,先获得锁才能释放锁,释放锁后进入等待队列。
    notify(),notifyAll()是将锁交给含有wait()方法的线程,让其继续执行下去,如果自身没有锁,怎么叫把锁交给其他线程呢;(本质是让处于阻塞队列的线程进入等待队列竞争锁)
    Synchronized应用举例:生产者消费者模型

消费者线程需要等待直到生产者线程完成一次写入操作。生产者线程需要等待消费者线程完成一次读取操作。假设没有应用Synchronized关键字,当消费者线程执行wait操作的同时,生产线线程执行notify,生产者线程可能在等待队列中找不到消费者线程。导致消费者线程一直处于阻塞状态。那么这个模型就要失败了。所以必须要加Synchronized关键字。

生产者消费者代码实现:

package p2;

//使用wait与notify实现
public class ProducerConsumer {

    public static void main(String[] arg) {

        Resource resource = new Resource();
        // 生产者线程
        ProducerThread p1 = new ProducerThread(resource);
        ProducerThread p2 = new ProducerThread(resource);
        ProducerThread p3 = new ProducerThread(resource);
        // 消费者线程。测试时可以少开几个消费线程看看具体
        ConsumerThread c1 = new ConsumerThread(resource);
        ConsumerThread c2 = new ConsumerThread(resource);
        ConsumerThread c3 = new ConsumerThread(resource);

        p1.start();
        p2.start();
        p3.start();
        c1.start();
        c2.start();
        c3.start();

    }

}

// 编写资源类
class Resource {
//当前资源池数量
    private int currentSize = 0;
    //允许数量
    private int allowSize = 10;

    // 取走资源,如果当前资源大于0则可以移除(消费),移除之后唤醒生产线程。否则进入等待释放线程资源
    public synchronized void remove() {
        if (currentSize > 0) {
            currentSize--;
            System.out.println(Thread.currentThread().getName() + "消费一件资源,当前资源池有" + currentSize + "个");

            notifyAll();
        } else {

            // 没有资源 消费者进入等待状态
            try {
                System.out.println(Thread.currentThread().getName() + "当前资源过少,等待增加");
                wait();
            } catch (InterruptedException e) {
                // TODO Auto-generated catch block
                e.printStackTrace();
            }

        }

    }

    public synchronized void add() {
        // 如果当前数量小于限制数量则可以增加,增加后唤醒消费者消费,否则等待消费,释放锁
        if (currentSize < allowSize) {
            currentSize++;
            System.out.println(Thread.currentThread().getName() + "生产一件资源,当前资源池有" + currentSize + "个");
            notifyAll();

        } else {

            try {
                System.out.println(Thread.currentThread().getName() + "当前资源过多,等待消费");
                wait();
            } catch (InterruptedException e) {
                // TODO Auto-generated catch block
                e.printStackTrace();
            }

        }

    }

}
//消费线程
class ConsumerThread extends Thread {
    private Resource resource;

    ConsumerThread(Resource resource) {
        this.resource = resource;
    }

    @Override
    public void run() {
        while (true) {
            //避免生产消费太快测试的时候看不到打印,休眠一秒
            try {
                Thread.sleep(1000);
            } catch (InterruptedException e) {
                e.printStackTrace();
            }
            //移除代表消费
            resource.remove();
        }

    }

}
//生产者线程
class ProducerThread extends Thread {
    private Resource resource;

    ProducerThread(Resource resource) {
        this.resource = resource;
    }

    @Override
    public void run() {
        while (true) {
            try {
                Thread.sleep(1000);
            } catch (InterruptedException e) {
                e.printStackTrace();
            }
            //生产
            resource.add();
        }

    }

}

三.volatile关键字
前面提到的synchronized关键字锁住的是代码块,但是容易造成资源的浪费,是一种重量锁,而volatile是一种轻量锁,锁住的是变量。
我们先来看下面的代码:

boolean value=false;
 --------------线程2------------- 
 public void change(){ value=true; }
  --------------线程1--------------
  public void output(){ if(value==true) System.out,println("输出成功"); }

上面的结果会输出什么呢?先执行线程2,再执行线程1,是否会输出成功呢?其实是不一定的,让我们来看下为什么:
在这里插入图片描述
上面这张图表明,不同线程在执行时,数据都是从主内存中取得的,每个线程自身都有一个工作内存。线程读入和写入数据的过程如下:
先从主内存中读取数据,放入工作内存,传递到线程中使用,在修改数据时,原路返回,经工作内存再写入到主内存中。
这样就会有问题了,当两个线程1先把修改的数据经过工作内存写回到主内存的过程中,线程二读取主内存中的数据了,这样数据就出现了不一致性,我们把这种情况叫做线程之间不可见性。
volatile关键字就是用来解决这种不可见问题的,它是怎么实现的呢?

  • 使用volatile修饰的变量在被一个线程修改后,直接将数据写回到主内存,跳过了工作内存
  • 使用volatile修饰的变量,在被线程1修改后,线程二中的该变量就被视为无效
  • 线程2中数据无效了,在使用的时候就必须重新回主内存中读取该数据

但是在使用volatile时需要注意一点,我们举个例子来说

public class Test implements Runnable {
	volatile int value=0;
	volatile int count=0;
	public void run(){
		for(int i=0;i<10000;i++){
			value++;
		}
		count++;
	}
	public static void main(String[] args) {
		// TODO Auto-generated method stub
		Test test=new Test();
		for(int i=0;i<5;i++){
			Thread t=new Thread(test);
			t.start();
		}
		while(test.count!=5);
		System.out.println(test.value);
        
	}

}

看下结果:
在这里插入图片描述
按理来说应该会输出50000,为什么会使那个结果呢?
原因在于volatile不能满足原子性。
原子性是指一个操作要么在执行时不被打断,要么就是不执行,原子操作只有最简单的赋值和读取。我们举例子讲解一下:

int a=1;
int b=a;
a++;

上面三个只有第一个是原子操作,第二个,先读取a的值,再赋值给b,两个原子操作叠加起来就不是原子操作,第三个,相当于a=a+1,先读取a的值,再加1,再赋值给a,也不是原子操作。
注意:long和double的变量是64位的,不满足原子操作。
了解了原子性我们想想为什么之前代码的结果不是500000。因为value的递增不是原子操作,volatile是无法保证原子性的。我们可以假设有这种情况,当value为100时线程1执行value自增的时候,比如说进行到了加1操作后被阻塞了,线程2接着进行value自增,线程2在主存中读取value值时会发现value还是100,那么线程1和线程2执行的结果都是101,相当于两次自增后value确只增加1,这就造成了实际值比500000小。

四.Lock
前面的synchronized加锁,只有加锁和释放所=锁,在JDK5后出现了新的加锁方法,使用Lock,包含比synchronized更多的加锁功能。ReentrantLock类是实现了Lock接口的锁.ReentrantLock类的常用方法:

ReentrantLock() : 创建一个ReentrantLock实例 
lock() : 获得锁 
unlock() : 释放锁 

两者区别:

  • synchronized是java的关键字,是java的内置特性,是基于JVM层面的,而Lock是接口,是基于javaJDK层面的,通过这个接口可以实现同步访问。
  • synchronized是不需要手动释放锁的,在代码执行完后,系统会让线程自动释放锁,但是Lock要手动解锁,如果不手动解锁,会出现死锁现象。
public class Test implements Runnable{
 Lock lock = new ReentrantLock();
  public void run(){
   lock.lock();
    try{
     String name=Thread.currentThread().getName(); 
     for(int i=0 ; i<5 ;i++){
     System.out.println("线程"+name+":"+i);
       } }
     catch(Exception e){
        }
      finally{ lock.unlock(); }
          } 
      public static void main(String[] args){ 
           Test test = new Test();
           Thread thread1 = new Thread(test,"1"); 
           thread1.start(); 
           Thread thread2 = new Thread(test,"2");
           thread2.start(); } }

结果如下:
在这里插入图片描述
可以看出线程按顺序执行了,通常将要锁住的代码和方法放在try-catch中,在finally中释放锁,和synchronized一样,是对同一对象的两个线程。

五.ThreadLocal类
使用ThreadLocal管理变量,每一个使用该变量的线程都获得该变量的副本,各个副本之间相互独立,每个线程都可以随意修改变量副本,而不会对其他线程造成影响。
ThreadLocal类的常用方法

ThreadLocal() : 创建一个线程本地变量 
get() : 返回此线程局部变量的当前线程副本中的值 
initialValue() : 返回此线程局部变量的当前线程的"初始值" 
set(T value) : 将此线程局部变量的当前线程副本中的值设置为value

来看一下如何使用:

public class Test implements Runnable{ 
private static ThreadLocal<Integer> value = new ThreadLocal<Integer>(){
 protected Integer initialValue() { 
 return 0; 
   } 
 };
 public void run(){
  for(int i=0 ; i<5 ;i++)
  value.set(value.get()+1); 
  String name = Thread.currentThread().getName(); 
  System.out.println("线程"+name+":"+value.get());
   }
    public static void main(String[] args){
    Test test = new Test(); 
    Thread thread1 = new Thread(test,"1");
    thread1.start(); 
    Thread thread2 = new Thread(test,"2");
     thread2.start(); } }

结果如下:
在这里插入图片描述
注:ThreadLocal与同步机制
a.ThreadLocal与同步机制都是为了解决多线程中相同变量的访问冲突问题。
b.前者采用以"空间换时间"的方法,后者采用以"时间换空间"的方式

  • 6
    点赞
  • 25
    收藏
    觉得还不错? 一键收藏
  • 0
    评论

“相关推荐”对你有帮助么?

  • 非常没帮助
  • 没帮助
  • 一般
  • 有帮助
  • 非常有帮助
提交
评论
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

当前余额3.43前往充值 >
需支付:10.00
成就一亿技术人!
领取后你会自动成为博主和红包主的粉丝 规则
hope_wisdom
发出的红包
实付
使用余额支付
点击重新获取
扫码支付
钱包余额 0

抵扣说明:

1.余额是钱包充值的虚拟货币,按照1:1的比例进行支付金额的抵扣。
2.余额无法直接购买下载,可以购买VIP、付费专栏及课程。

余额充值