Java多线程实践之—同步篇

上一篇介绍了线程相关的概念,以及一些线程的常规方法,今天学习一下线程的同步的一些知识,这也是多线程中极为重要的一部分!

1. 为什么要同步?

多个线程会同时访问一个公共资源(因为线程之间是共享资源的,而进程之间则是单独拥有一部分资源的),这种情况称为竞争状态。如果一个类的对象在多线程程序中没有导致竞争状态,则称之为线程安全的,否则就是线程不安全!

2. 如何解决同步问题?

Java提供内置支持关键字synchronized,用于保护共享资源。共享资源一般是以对象形式存在的内存片段,也可以是文件、输入/ 输出端口、或者是打印机。要控制对共享资源的访问,得先把它包装进一个对象,然后把所有要访问这个对象的方法标记为synchronized,当有一个线程在调用synchronized方法时,其他调用类中synchronized方法的线程都会被阻塞,普通方法不会受影响!

注意:在使用并发时,将域(指的是共享资源,可以简单理解为成员变量)设置为private非常重要,因为这样一来其他任务就可以直接访问域,从而产生冲突!

3. 什么时候需要同步?

Brian同步规则:如果你正在写一个变量,它可能接下来被另一个线程读取,或者正在读取上一次已经被另一个线程写过的变量,那么必须使用同步!

注意:如果在一个类中有超过一个方法在处理临界(共享资源区,即临界区)数据,那么必须同步所有相关的方法。

一个小示例,启动50个线程,每个线程往一个账户里添加1块钱。那么按照逻辑,当所有的线程执行介绍,账户里应该有50块钱,来看看结果:

package multithreading;

import java.util.concurrent.*;

public class AccountWithSync {
	
	private static Account account = new Account();
	
	public static void main(String[] args) {
		ExecutorService executorService = Executors.newCachedThreadPool();
		
		for (int i = 0; i < 50; i++) {
			executorService.execute(new AddPennyTask());
		}
		executorService.shutdown();
		while (!executorService.isTerminated()) {
		}		
		System.out.println("What is balance? " + account.getBalance());
	}
	
	private static class AddPennyTask implements Runnable {
		public void run() {
			account.deposit(1);
		}
	}
	
	private static class Account {
		private int balance = 0;
		public int getBalance() {
			return balance;
		}
		public void deposit(int amount) {
			int newBalance = balance + amount;
			try {
				Thread.sleep(5);
			} catch (InterruptedException e) {
			}
			balance = newBalance;
		}
	}
}
这段程序的运行结果将是不可预测的,每次的值都会变,就是因为没有同步deposit()方法,下面提供两种添加synchronized的方法:

(1)将23行代码修改成这样(这是同步语句,也称同步块):

synchronized (account) {
<span style="white-space:pre">	</span>account.deposit(1);
}
(2)将32行改成这样(这是同步方法):
public synchronized void deposit(int amount)
提示同步块方法会更加有效!

另一种同步的方法(使用Lock对象,显示的加锁同步)

package multithreading;

import java.util.concurrent.*;
import java.util.concurrent.locks.*;

public class AccountWithSyncUsingLock {
	
	private static Account account = new Account();
	
	public static void main(String[] args) {
		
		ExecutorService executorService = Executors.newCachedThreadPool();				
		for (int i = 0; i < 50; i++)
			executorService.execute(new AddPennyTask());
		executorService.shutdown();	
		//while语句很重要,不写则会出现线程还没执行完就输出结果了!
		//因为shutdown()只是告诉执行器不在接受新的线程,但原有的线程还在执行!
		while (!executorService.isTerminated()) {
		}
		System.out.println("What is balance? " + account.getBalance());
	}
	private static class AddPennyTask implements Runnable {
		public void run() {
			account.deposit(1);
		}
	}
	private static class Account {
		
		private static Lock lock = new ReentrantLock();
		private int balance = 0;
		
		public int getBalance() {
			return balance;
		}
		
		public void deposit(int amount) {
			lock.lock();		
			try {
				int newBalance = balance + amount;
				Thread.sleep(5);
				balance = newBalance;
			} catch (InterruptedException e) {
			}
			finally {
				lock.unlock();
			}		
		}
	}
}
提示:在对lock()的调用之后紧随一个try-catch块并且在finally子句中释放这个锁这个习惯很好,确保锁被释放。《Think in Java》中提到:lock()方法必须在try-finally语句中调用,return语句必须在try中出现,以确保unlock()不会过早发生

synchronized和Lock的对比:《Think in Java》比较赞成使用synchronized,当需要性能优化时就使用Lock。但是给出的理由不是那么有说服力,代码多也成为了一个缺点。确实是,多了几行,对象的创建、锁的创建、try-catch-finally语句,以及释放。查了一些资料,发现Lock的功能强大的多,比如为读写分别提供了锁!所以个人想法是,当能用synchronized解决问题的就用synchronized,不行则用Lock,似乎《Think in Java》讲的是对的,大牛毕竟是大牛!

4. 原子性的讨论

原子操作是不能被线程调度机制中断的操作,一旦开始操作,那么在“上下文”切换(线程切换)之前执行完成。

误区提示:“原子操作不需要进行同步控制”是不正确的!

原子性可以应用于除long和double之外的所有基本类型之上的“简单操作”,JVM会64位的数据分离成两个32位的操作,这就有了一个上下文切换,会造成不安全!在定义long和double时可以加上volatile,那么就会获得原子性(简单的赋值和返回操作)。

提示:不要依赖于原子性。基本类型的读取和赋值操作被认为是安全的原子性操作,但是当对象处于不稳定状态是不安全的,下面是一个示例:

package multithreading;

import java.util.concurrent.*;

public class AtomicityTest implements Runnable {
  private int i = 0;
  public int getValue() { return i; }
  private synchronized void evenIncrement() { i++; i++; }
  public void run() {
    while(true)
      evenIncrement();
  }
  public static void main(String[] args) {
    ExecutorService exec = Executors.newCachedThreadPool();
    AtomicityTest at = new AtomicityTest();
    exec.execute(at);
    while(true) {
      int val = at.getValue();
      if(val % 2 != 0) {
        System.out.println(val);
        System.exit(0);
      }
    }
  }
}
按照逻辑,这段代码是死循环,但是现在却有输出!说明 i++递增操作不是原子性

下面的代码使用了AtomicInteger原子类(还有其他原子类),取消了synchronized的使用,也达到了效果:

private AtomicInteger j = new AtomicInteger(0);
  public int getValue() { return j.get(); }
  private void evenIncrement() { j.addAndGet(2); }
  public void run() {
    while(true)
      evenIncrement();
  }

网络资料:Java 理论与实践: 正确使用 Volatile 变量

提示volatile声明的变量,只有赋值和读取时,不需要同步对其访问,其他操作依旧需要同步访问!

5. 解决共享资源的另一种方法

在第3节中讨论了加锁的机制来防止在共享资源上产生冲突,第二种方式是根除对变量的共享:线程本地存储。

package zy.thread.demo;

import java.util.Random;
import java.util.concurrent.*;

public class ThreadLocalVariableHolder {

	private static ThreadLocal<Integer> value = new ThreadLocal<Integer>() {
		private Random rand = new Random(47);
		protected synchronized Integer initialValue() {
			return rand.nextInt(100);
		}
	};
	public static void increment() {
		value.set(value.get() + 1);
	}
	public static int get() { return value.get(); }
	public static void main(String[] args) throws Exception {
		   ExecutorService exec = Executors.newCachedThreadPool();
		   for(int i = 0; i < 5; i++)
			   exec.execute(new Accessor(i));
		   TimeUnit.SECONDS.sleep(1);
		   exec.shutdownNow();         
		  }
}
class Accessor implements Runnable {
	  private final int id;
	  public Accessor(int idn) { id = idn; }
	  public void run() {
	    while(!Thread.currentThread().isInterrupted()) {
	      ThreadLocalVariableHolder.increment();
	      System.out.println(this);
	      Thread.yield();
	    }
	  }
	  public String toString() {
	    return "#" + id + ": " +
	      ThreadLocalVariableHolder.get();
	  }
	}

下一节介绍任务的终结和线程之间的协作

  • 1
    点赞
  • 2
    收藏
    觉得还不错? 一键收藏
  • 0
    评论
评论
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

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

抵扣说明:

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

余额充值