java类内多个函数如何同步
Posted
tags:
篇首语:本文由小常识网(cha138.com)小编为大家整理,主要介绍了java类内多个函数如何同步相关的知识,希望对你有一定的参考价值。
比如我定义了方法A和方法B,当实例化类后,多线程如何保证调用方法A时方法B即使被调用也不会被执行?Java有保证这种机制的关键字吗?
线程间的通讯首要的方式就是对字段及其字段所引用的对象的共享访问。这种通信方式是及其高效的,但是也是导致了可能的错误:线程间相互干涉和内存一致性的问题。避免出现这两种错误的方法就是同步。线程间相互干扰描述了当多个线程访问共享数据时可能出现的错误。
内存一致性错误描述的了共享内存可能导致的错误。
同步方法(Synchronized method)描述了一种简单的可以有效防止线程间相互干扰及其内存一致性错误的方法。
明锁及同步描述了一种更加通用的同步方法,以及同步是如何基于明锁而实现的。
原子性描述了不能被其它线程干扰的操作。
线程间的相互干扰
考虑下面的一个简单的类Counter:
[java] view plaincopy
class Counter
private int c = 0;
public void increment()
c++;
public void decrement()
c--;
public int value()
return c;
不难看出,其中的increment()方法用来对c加1;decrement()方法用来对c减1。然而,有多个线程中都存在对某个Counter对象的引用,那么线程间的干扰就可能导致出现我们不想要的结果。
线程间的干扰出现在多个线程对同一个数据进行多个操作的时候,也就是出现了“交错”。这就意味着操作是由多个步骤构成的,而此时,在这多个步骤的执行上出现了叠加。
Counter类对象的操作貌似不可能出现这种“交错”,因为其中的两个关于c的操作都很简单,只有一条语句。然而,即使是一条语句也是会被VM翻译成多个步骤的。在这里,我们不深究VM具体上上面的操作翻译成了什么样的步骤。只需要知道即使简单的c++这样的表达式也是会被翻译成三个步骤的:
1. 获取c的当前值。
2. 对其当前值加1。
3. 将增加后的值存储到c中。
表达式c--也是会被按照同样的方式进行翻译,只不过第二步变成了减1,而不是加1。
假定线程A中调用increment方法,线程B中调用decrement方法,而调用时间基本上相同。如果c的初始值为0,那么这两个操作的“交错”顺序可能如下:
1. 线程A:获取c的值。
2. 线程B:获取c的值。
3. 线程A:对获取到的值加1;其结果是1。
4. 线程B:对获取到的值减1;其结果是-1。
5. 线程A:将结果存储到c中;此时c的值是1。
6. 线程B:将结果存储到c中;此时c的值是-1。
这样线程A计算的值就丢失了,也就是被线程B的值覆盖了。上面的这种“交错”只是一种可能性。在不同的系统环境中,有可能是B线程的结果丢失了,或者是根本就不会出现错误。由于这种“交错”是不可预测的,线程间相互干扰造成的缺陷是很难定位和修改的。
内存一致性错误
内存一致性错误发生在不同线程对同一数据产生不同的“见解”。导致内存一致性错误的原因很负责,超出了本文的描述范围。庆幸的是,程序员并不需要知道出现这些原因的细节。我们需要的只是一种可以避免这种错误的方法。
避免出现内存一致性错误的关键在于理解“先后顺序”关系。这种关系是一种简单的方法,能够确保一条语句对内存的写操作对于其它特定的语句都是可见的。为了理解这点,我们可以考虑如下的示例。假定定义了一个简单的int类型的字段并对其进行了初始化:
int counter = 0;
该字段由两个线程共享:A和B。假定线程A对counter进行了自增操作:
counter++;
然后,线程B输出counter的值:
System.out.println(counter);
如果以上两条语句是在同一个线程中执行的,那么输出的结果自然是1。但是如果这两条语句是在两个不同的线程中,那么输出的结构有可能是0。这是因为没有保证线程A对counter的修改对线程B来说是可见的。除非程序员在这两条语句间建立了一定的“先后顺序”。
我们可以采取多种方式建立这种“先后顺序”。使用同步就是其中之一,这点我们将会在下面的小节中看到。
到目前为止,我们已经看到了两种建立这种“先后顺序”的方式:
当一条语句中调用了Thread.start()方法,那么每一条和该语句已经建立了“先后顺序”的语句都和新线程中的每一条语句有着这种“先后顺序”。引入并创建这个新线程的代码产生的结果对该新线程来说都是可见的。
当一个线程终止了并导致另外的线程中调用join的语句回,那么此时这个终止了的线程中执行了的所有语句都和随后的join语句随后的所有语句建立了这种“先后关系”。也就是说终止了的线程中的代码效果对调用join方法的线程来说是可见。
关于哪些操作可以建立这种“先后关系”,更多的信息请参阅“java.util.concurrent包的概要说明”。
同步方法
Java编程语言中提供了两种基本的同步用语:同步方法和同步语句。同步语句相对而言更为复杂一些,我们将在下一小节中进行描述。本节重点讨论同步方法。
我们只需要在声明方法的时候增加关键字synchronized即可:
[java] view plaincopy
public class SynchronizedCounter
private int c = 0;
public synchronized void increment()
c++;
public synchronized void decrement()
c--;
public synchronized int value()
return c;
如果count 是SynchronizedCounter类的实例,设置其方法为同步方法将有一下两个效果:
首先,不可能出现对同一对象的同步方法的两个调用的“交错”。当一个线程在执行一个对象的同步方式的时候,其他所有的调用该对象的同步方法的线程对会被挂起,直到第一个线程对该对象操作完毕。
其次,当一个同步方法退出时,会自动与该对象的后续同步方法间建立“先后顺序”的关系。这就确保了对该对象的修改对其他线程是可见的。
注意:构造函数不能为同步的——在构造函数前使用synchronized关键字将导致语义错误。同步构造函数是没有意义的。这是因为只有创建该对象的线程才能调用其构造函数。
警告:在创建多个线程共享的对象时,要特别小心对该对象的引用不能过早地“泄露”。例如,假定我们想要维护一个保存类的所有实例的列表instances。我们可能会在构造函数中这样写到:
instances.add(this);
但是,其他线程可会在该对象的构造完成之前就访问该对象。
同步方法是一种简单的可以避免线程相互干扰和内存一致性错误的策略:如果一个对象对多个线程都是可见的,那么所有对该对象的变量的读写都应该是通过同步方法完成的(一个例外就是final字段,他在对象创建完成后是不能被修改的,因此,在对象创建完毕后,可以通过非同步的方法对其进行安全的读取)。这种策略是有效的,但是可能导致“liveness”问题。这点我们会在本课程的后面进行描述。
内部锁及同步
同步是构建在被称为“内部锁或者是监视锁”的内部实体上的。(在API中通常被称为是“监视器”。)内部锁在两个方面都扮演着重要的角色:保证对对象访问的排他性和建立也对象可见性相关的重要的“先后顺序”。
每一个对象都有一个与之相关联动的内部锁。按照传统的做法,当一个线程需要对一个对象的字段进行排他性访问并保持访问的一致性时,他必须在访问前先获取该对象的内部锁,然后才能访问之,最后释放该内部锁。在线程获取对象的内部锁到释放对象的内部锁的这段时间,我们说该线程拥有该对象的内部锁。只要有一个线程已经拥有了一个内部锁,其他线程就不能在拥有该锁了。其他线程将会在试图获取该锁的时候被阻塞了。
当一个线程释放了一个内部锁,那么就会建立起该动作和后续获取该锁之间的“先后顺序”。
同步方法中的锁
当一个线程调用一个同步方法的时候,他就自动地获得了该方法所属对象的内部锁,并在方法返回的时候释放该锁。即使是由于出现了没有被捕获的异常而导致方法返回,该锁也会被释放。
我们可能会感到疑惑:当调用一个静态的同步方法的时候会怎样了,静态方法是和类相关的,而不是和对象相关的? 在这种情况下,线程获取的是该类的类对象的内部锁。这样对于静态字段的方法是通过一个和类的实例的锁相区分的另外的锁来进行的。
同步语句
另外一种创建同步代码的方式就是使用同步语句。和同步方法不同,使用同步语句是必须指明是要使用哪个对象的内部锁:
[java] view plaincopy
public void addName(String name)
synchronized(this)
lastName = name;
nameCount++;
nameList.add(name);
在上面的示例中,方法addName需要对lastName和nameCount的修改进行同步,还要避免同步调用其他对象的方法(在同步代码段中调用其他对象的方法可能导致“Liveness”中描述的问题)。如果没有使用同步语句,那么将不得不使用一个单独的,未同步的方法来完成对nameList.add的调用。
在改善并发性时,巧妙地使用同步语句能起到很大的帮助作用。例如,我们假定类MsLunch有两个实例字段,c1和c2,这两个变量绝不会一起使用。所有对这两个变量的更新都需要进行同步。但是没有理由阻止对c1的更新和对c2的更新出现交错——这样做会创建不必要的阻塞,进而降低并发性。此时,我们没有使用同步方法或者使用和this相关的锁,而是创建了两个单独的对象来提供锁。
[java] view plaincopy
public class MsLunch
private long c1 = 0;
private long c2 = 0;
private Object lock1 = new Object();
private Object lock2 = new Object();
public void inc1()
synchronized(lock1)
c1++;
public void inc2()
synchronized(lock2)
c2++;
采用这种方式时需要特别的小心。我们必须绝对确保相关字段的访问交错是完全安全的。
同步的重入
回忆前面提到的:线程不能获取已经被别的线程获取的锁。单丝线程可以获取自身已经拥有的锁。允许一个线程能重复获得同一个锁就称为同步重入。它是这样的一种情况:在同步代码中直接或者间接地调用了还有同步代码的方法,两个同步代码段中使用的是同一个锁。如果没有同步重入,在编写同步代码时需要额外的小心,以避免线程将自己阻塞。
原子性
在编程中,原子性动作就是指一次性有效完成的动作。原子性动作是不能在中间停止的:要么一次性完全执行完毕,要么就不执行。在动作没有执行完毕之前,是不会产生可见结果的。
通过前面的示例,我们已经发现了诸如c++这样的自增表达式并不属于原子操作。即使是非常见到的表达式也定义了负责的动作,这些动作可以被解释成许多别的动作。然而,的确存在一些原子操作的:
对几乎所有的原生数据类型变量的读写(除了long和都变了外)以及引用变量的读写都是原子的。
对所有声明为volatile的变量的读写都是原子的,包括long和double类型。
原子性动作是不会出现交错的,因此,使用这些原子性动作时不用考虑线程间的干扰。然而,这并不意味着可以移除对原子操作的同步。因为内存一致性错误还是有可能出现的。使用volatile变量可以减少内存一致性错误的风险,因为任何对volatile变量的写操作都和后续对该变量的读操作建立了“先后顺序”。这就意味着对volatile类型变量的修改对于别的线程来说是可见的。更重要的是,这意味着当一个线程读取一个volatile类型的变量是,他看到的不仅仅是对该变量的最后一次修改,还看到了导致这种修改的代码带来的其他影响。
使用简单的原子变量访问比通过同步代码来访问变量更高效,但是需要程序员的更多细心考虑,以避免内存一致性错误。这种额外的付出是否值得完全取决于应用程序的大小和复杂度。
在包java.util.concurrent中的一些类提供了原子方法,这些方法不依赖于同步。我们会在章节:High Level Concurrency Objects中进行讨论。 参考技术A 第一步是让线程同步,第二部是让线程有顺序。
同步:我们可以用synchronized来解决。
java线程同步原理: java会为每个object对象分配一个monitor,当某个对象的同步方法(synchronized methods )被多个线程调用时,该对象的monitor将负责处理这些访问的并发独占要求。
当一个线程调用一个对象的同步方法时,JVM会检查该对象的monitor。如果monitor没有被占用,那么这个线程就得到了monitor的占有权,可以继续执行该对象的同步方法;如果monitor被其他线程所占用,那么该线程将被挂起,直到monitor被释放。
当线程退出同步方法调用时,该线程会释放monitor,这将允许其他等待的线程获得monitor以使对同步方法的调用执行下去。就像下面这样:
public void test()
synchronized (this)
//做一些事
//这里只会有一个线程来调用该方法,因为只有一个this对象作为资源分配给该线程
顺序:我们可以用List来解决,因为它是有序的。我们只需要将要执行的线程放入到List中不就好解决了吗
上代码:
/**
* 让多个线程同步顺序执行的线程管理器
* @author bianrx
* @date 2012.1.18
* SynchronizedRunMultiThread
*/
public class SyncManager
/**
* 待执行的线程集合,注意这里必须是Runnable接口类型,下面会解释
*/
private List<Runnable> runnableList;
public SyncManager()
public SyncManager(List<Runnable> runnableList)
this.runnableList = runnableList;
public void setRunnable(List<Runnable> runnableList)
this.runnableList = runnableList;
public void run()
//遍历代执行的线程集合
for(Runnable runnable: runnableList)
runThread(runnable);
/**
* 按顺序同步执行多个线程
* @author bianrx
* @date 2012.1.18
* @param runnable
*/
private void runThread(Runnable runnable)
synchronized (this)
runnable.run();//这里需要注意的是:必须调用run方法,因为如果你调用了start方法,线程只会向JVM请求资源,但是未必就执行其中的run。
//这个方法是同步的,所以当前只有一个线程占用了this对象。
测试代码:有两个要执行的线程
public class Thread1 extends Thread
@Override
public void run()
System.out.println("运行线程1");
public class Thread2 extends Thread
@Override
public void run()
System.out.println("运行线程2");
//主函数测试
public class MainTest
/**
* @param args
* @throws ParseException
* @throws InterruptedException
*/
public static void main(String[] args) throws ParseException, InterruptedException
List<Runnable> runnableList = new ArrayList<Runnable>();
runnableList.add(new Thread1());
runnableList.add(new Thread2());
while(true)
Thread.sleep(1000);
new SyncManager(runnableList).run();
System.out.println("---------------");
参考技术B 如果是A B 是两个单独的方法,没有任何关联 那么基本上是没法实现的..
除非你还有一个工具类 来控制调用类X中的A B方法,那么同步就要加在这个工具类中了
如果是A方法中调用B 那么就在调用B的地方 加一个同步代码块 Synchronized(this)调用B 可以保证每次只有一个线程访问 调用B 处的代码 参考技术C 用 sychronized 修饰方法 参考技术D 1.用 synchronized 修饰方法
解决方案1:
public static void method0()
synchronized(类名.class)
//需要被同步的代码
同一个类中的所有synchronized修饰的方法是不能同时调用的,也就是说同时只能调用其中一个方法,比如线程1调用A方法,在A方法执行完之前,线程2调用B方法,这个时候线程2就会阻塞,直到线程1调用完A方法后,线程2才开始执行B方法!
2.加一个同步对象锁
就是加一个同步对象锁
public class O
Object lock,在A方法执行完之前,线程2才开始执行B方法;
public void A()
synchronized(lock)
/:
public class O
public synchronized void A()
public synchronized void B()
同一个类中的所有synchronized修饰的方法是不能同时调用的;这里写方法内容
public void B()
synchronized(lock)
/,线程2调用B方法,比如线程1调用A方法,也就是说同时只能调用其中一个方法,这个时候线程2就会阻塞;/,直到线程1调用完A方法后;/用 synchronized 修饰方法。
java多线程中.同步代码块和同步函数的区别:
同步代码块一般更好,只是同步函数,作用范围大,效率低下.同步代码块效率高些,但是要求你将可能并发出syn,写起来比同步函数稍微难写点.
Java技术学习:如何保证同一资源被多个线程并发访问时的完整性?
常用的同步方法是采用信号或加锁机制,保证资源在任意时刻至多被一个线程访问。Java语言在多线程编程上实现了完全对象化,提供了对同步机制的良好支持。
在Java中一共有四种方法支持同步,其中前三个是同步方法,一个是管道方法。管道方法不建议使用,阻塞队列方法在之前已有描述,现只提供前两种实现方法。
- wait()/notify()方法
- await()/signal()方法
- BlockingQueue阻塞队列方法
- PipedInputStream/PipedOutputStream
一、生产者类:
```
public class Producer extends Thread { // 每次生产的产品数量
private int num;
// 所在放置的仓库
private Storage storage;
// 构造函数,设置仓库
public Producer(Storage storage) {
this.storage = storage;
}
// 线程run函数
public void run() {
produce(num);
}
// 调用仓库Storage的生产函数
public void produce(int num) {
storage.produce(num);
}
public int getNum() {
return num;
}
public void setNum(int num) {
this.num = num;
}
public Storage getStorage() {
return storage;
}
public void setStorage(Storage storage) {
this.storage = storage;
}
}
```
二、消费者类:
```
public class Consumer extends Thread { // 每次消费的产品数量
private int num;
// 所在放置的仓库
private Storage storage;
// 构造函数,设置仓库
public Consumer(Storage storage) {
this.storage = storage;
}
// 线程run函数
public void run() {
consume(num);
}
// 调用仓库Storage的生产函数
public void consume(int num) {
storage.consume(num);
}
// get/set方法
public int getNum() {
return num;
}
public void setNum(int num) {
this.num = num;
}
public Storage getStorage() {
return storage;
}
public void setStorage(Storage storage) {
this.storage = storage;
}
}
```
仓库类:(wait()/notify()方法)
```
public class Storage { // 仓库最大存储量
private final int MAX_SIZE = 100;
// 仓库存储的载体
private LinkedList<Object> list = new LinkedList<Object>();
// 生产num个产品
public void produce(int num) {
// 同步代码段
synchronized (list) {
// 如果仓库剩余容量不足
while (list.size() + num > MAX_SIZE) {
System.out.print("【要生产的产品数量】:" + num);
System.out.println(" 【库存量】:" + list.size() + " 暂时不能执行生产任务!");
try {
list.wait();// 由于条件不满足,生产阻塞
} catch (InterruptedException e) {
e.printStackTrace();
}
}
// 生产条件满足情况下,生产num个产品
for (int i = 1; i <= num; ++i) {
list.add(new Object());
}
System.out.print("【已经生产产品数】:" + num);
System.out.println(" 【现仓储量为】:" + list.size());
list.notifyAll();
}
}
// 消费num个产品
public void consume(int num) {
// 同步代码段
synchronized (list) {
// 如果仓库存储量不足
while (list.size() < num) {
System.out.print("【要消费的产品数量】:" + num);
System.out.println(" 【库存量】:" + list.size() + " 暂时不能执行生产任务!");
try {
// 由于条件不满足,消费阻塞
list.wait();
} catch (InterruptedException e) {
e.printStackTrace();
}
}
// 消费条件满足情况下,消费num个产品
for (int i = 1; i <= num; ++i) {
list.remove();
}
System.out.print("【已经消费产品数】:" + num);
System.out.println(" 【现仓储)量为】:" + list.size());
list.notifyAll();
}
}
// get/set方法
public LinkedList<Object> getList() {
return list;
}
public void setList(LinkedList<Object> list) {
this.list = list;
}
public int getMAX_SIZE() {
return MAX_SIZE;
}
}
```
仓库类:(await()/signal()方法)
```
public class Storage { // 仓库最大存储量
// 仓库最大存储量
private final int MAX_SIZE = 100;
// 仓库存储的载体
private LinkedList<Object> list = new LinkedList<Object>();
// 锁
private final Lock lock = new ReentrantLock();
// 仓库满的条件变量
private final Condition full = lock.newCondition();
// 仓库空的条件变量
private final Condition empty = lock.newCondition();
// 生产num个产品
public void produce(int num) {
// 获得锁
lock.lock();
// 如果仓库剩余容量不足
while (list.size() + num > MAX_SIZE) {
System.out.print("【要生产的产品数量】:" + num);
System.out.println(" 【库存量】:" + list.size() + " 暂时不能执行生产任务!");
try {
// 由于条件不满足,生产阻塞
full.await();
} catch (InterruptedException e) {
e.printStackTrace();
}
}
// 生产条件满足情况下,生产num个产品
for (int i = 1; i <= num; ++i) {
list.add(new Object());
}
System.out.print("【已经生产产品数】:" + num);
System.out.println(" 【现仓储量为】:" + list.size());
// 唤醒其他所有线程
full.signalAll();
empty.signalAll();
// 释放锁
lock.unlock();
}
// 消费num个产品
public void consume(int num) {
// 获得锁
lock.lock();
// 如果仓库存储量不足
while (list.size() < num) {
System.out.print("【要消费的产品数量】:" + num);
System.out.println(" 【库存量】:" + list.size() + " 暂时不能执行生产任务!");
try {
// 由于条件不满足,消费阻塞
empty.await();
} catch (InterruptedException e) {
e.printStackTrace();
}
}
// 消费条件满足情况下,消费num个产品
for (int i = 1; i <= num; ++i) {
list.remove();
}
System.out.print("【已经消费产品数】:" + num);
System.out.println(" 【现仓储)量为】:" + list.size());
// 唤醒其他所有线程
full.signalAll();
empty.signalAll();
// 释放锁
lock.unlock();
}
// set/get方法
public int getMAX_SIZE() {
return MAX_SIZE;
}
public LinkedList<Object> getList() {
return list;
}
public void setList(LinkedList<Object> list) {
this.list = list;
}
}
以上是关于java类内多个函数如何同步的主要内容,如果未能解决你的问题,请参考以下文章