`

【线程】使用同步:解决多线程引发的内存一致性错误问题

    博客分类:
  • Java
 
阅读更多

首先,需要理解一些理论上的东西。

 

多个线程并发对同一个资源进行操作,很可能发生内存一致性错误

 

究其原因,线程很多情况下对资源的操作不是原子的,这些代码会被分为若干条指令去执行,而在一个CPU时间片内又不能将这些指令全部执行完毕。

当多个线程同时操作同一个共享资源时,线程B拿着线程A的半成品进行操作,内存一致性错误就发生了。

 

如何解决?

1.同步,即:加锁。通过加锁的方式,可以确保唯一获取锁的线程可以不受干扰的执行完自己的程序片段。只有当前线程释放锁之后,其它线程才能对此资源进行操作。否则,只能等待当前线程执行完毕后释放锁(等待锁的线程全部被阻塞了);

 

2.资源不可变,即:只读。只读意味着数据不会变化,因此对只读数据的操作不可能发生内存不一致问题。

 

同步锁

同步机制的建立是基于其内部一个叫内部锁或者监视锁的实体。

内部锁在同步机制中起到两方面的作用:

对一个对象的排他性访问;

建立一种happens-before关系,而这种关系正是可见性问题的关键所在。

每个对象都有一个与之关联的内部锁。
当一个线程需要排他性的访问一个对象的域时,首先请求该对象的内部锁,当访问结束时释放内部锁。
在线程获得内部锁到释放内部锁的这段时间里,只有当前线程拥有这个内部锁。
当一个线程拥有一个内部锁时,其他线程将无法获得该内部锁。其他线程如果去尝试获得该内部锁,则会被阻塞。
当线程释放其拥有的内部锁时,该操作和对该锁的后续请求间将建立happens-before关系。

 

 

同步(synchronized)的两种方式:

1. 同步方法

非静态方法-this

当线程调用一个同步方法时,它会自动请求该方法所在对象的内部锁。
当方法返回结束时则自动释放该内部锁,即使退出是由于发生了未捕获的异常,内部锁也会被释放。
静态方法-Class
当调用一个静态的同步方法时,由于静态方法是和类(而不是对象)相关的,所以线程会请求类对象(Class Object)的内部锁。
因此用来控制类的静态域访问的锁不同于控制对象访问的锁。

 

2. 同步代码块

同步块必须指定所请求的是哪个对象的内部锁

可以实现更细粒度的控制

 

可重入同步

一个线程不能获得其他线程所拥有的锁,但是它可以获得自己已经拥有的锁。
允许一个线程多次获得同一个锁实现了可重入同步。
避免了线程自己阻塞自己。

 

 

线程同步可以解决内存不一致错误,但也引入了其它的问题

死锁、饥饿、活锁

 

A.线程死锁

两个或多个线程永久阻塞,互相等待对方释放资源。

 

package org.thread;
public class DeadLock {
	
	public static void main(String[] args) {
		final Friend zs = new Friend("zs");
		final Friend ls = new Friend("ls");
		
		new Thread(new Runnable() {
			@Override
			public void run() {
				while(true)
					zs.bow(ls);
			}
		}).start();
		
		new Thread(new Runnable() {
			@Override
			public void run() {
				while(true)
					ls.bow(zs);
			}
		}).start();
		
	}
	
	static class Friend {
		private String name;
		
		public Friend(String name) {
			this.name = name;
		}
		
		public synchronized void bow(Friend friend) {
			System.out.println(Thread.currentThread().getName()+":::"+this.name+" bow to " + friend.name);
			friend.bowBack(this);//同步中调用其它对象的同步方法可能会发生死锁:相互等待对方释放锁
			System.out.println(Thread.currentThread().getName()+":::"+"bow done!");
		}
		
		public synchronized void bowBack(Friend friend) {
			System.out.println(Thread.currentThread().getName()+":::"+this.name+" bow to " + friend.name);
		}
	}
}

 

引起死锁的原因:需要相互获取对方锁的线程同时占有了自己的锁,导致对方无法获取到锁。

解决办法:通过设置同一个锁实现排他性访问,不给线程同时占有锁的机会。

 

package org.thread;
public class AvoidDeadLock {
	
	public static void main(String[] args) {
		final Friend zs = new Friend("zs");
		final Friend ls = new Friend("ls");
		
		new Thread(new Runnable() {
			@Override
			public void run() {
				while(true)
					zs.bowEachOther(ls);
			}
		}).start();
		
		new Thread(new Runnable() {
			@Override
			public void run() {
				while(true)
					ls.bowEachOther(zs);
			}
		}).start();
		
	}
	
	static class Friend {
		private String name;
		
		public Friend(String name) {
			this.name = name;
		}
		
		public synchronized void bow(Friend friend) {
			System.out.println(Thread.currentThread().getName()+":::"+this.name+" bow to " + friend.name);
			friend.bowBack(this);//同步中调用其它对象的同步方法可能会发生死锁:相互等待对方释放锁
			System.out.println(Thread.currentThread().getName()+":::"+"bow done!");
		}
		
		public synchronized void bowBack(Friend friend) {
			System.out.println(Thread.currentThread().getName()+":::"+this.name+" bow to " + friend.name);
		}
		//额外引入一个能让线程排他性访问的方法,这些线程需要争夺同一个锁
		public void bowEachOther(Friend friend) {
			//使用同一个锁,实现排他性的访问
			synchronized (Friend.class) {
				bow(friend);
			}
		}
	}
}

 

B.线程饥饿

当某个线程占有了锁,并且需要执行很长一段时间才释放锁,这就导致其它等待锁释放的线程处于阻塞状态。这种情况下其它线程便处于“饥饿”状态。

 

C.线程活锁

活锁指的是线程间相互响应时,由于响应结果不正确而导致彼此一直都处于响应状态。

线程并没有阻塞,只是一直在响应而无法恢复到正常的工作中。

 

 

线程协作

实际开发中,很多场景都可以归结为生产者-消费者的协作关系。

基本规则:

任务池满的时候,阻塞生产者,直到任务池中有任务被取走;

任务池空的时候,阻塞消费者,直到任务池增加了新的任务;

实现原理:

1. 同步:生产者与消费者使用同一个锁

2. 协作:

wait()  线程判断条件不满足时,等待

notify()/notifyAll()  解除对方的等待

 

以下示例通过synchronized、while->wait()、notifyAll()模拟生产者-消费者的模型。

实际开发中,不需要自己再去发明轮子了,请使用java.util.concurrent包中的工具类完成需要的功能

 

package org.thread;
import java.util.concurrent.ThreadLocalRandom;

public class ConsumerProduerDemo {
	public static void main(String[] args) {
		Messenger messenger = new Messenger();
		new Consumer(messenger).start();
		new Producer(messenger).start();
	}

}

// ---共享资源:message
class Messenger {
	private String message;
	boolean empty = true;

	public synchronized String take() {
		while (empty) {
			try {
				System.out.println("Consumer waiting...");
				wait();
			} catch (InterruptedException e) {
			}
		}
		empty = true;
		notifyAll();
		return message;
	}

	public synchronized void put(String message) {
		while (!empty) {
			try {
				System.out.println("Producer waiting...");
				wait();
			} catch (InterruptedException e) {}
		}
		this.message = message;
		empty = false;
		notifyAll();
	}
}

// ---生产者
class Producer implements Runnable {

	private Messenger messenger;

	public Producer(Messenger messenger) {
		this.messenger = messenger;
	}

	@Override
	public void run() {
		String[] msgs = { "First msg", "Second msg", "Third msg", "Fouth msg" };
		for (int i = 0; i < msgs.length; i++) {
			messenger.put(msgs[i]);
			try {
				Thread.sleep(ThreadLocalRandom.current().nextInt(1000));
			} catch (InterruptedException e) {
			}
		}
		messenger.put("Done");
	}

	public Thread start() {
		Thread t = new Thread(this);
		t.start();
		return t;
	}
}

// ---消费者
class Consumer implements Runnable {

	private Messenger messenger;

	public Consumer(Messenger messenger) {
		this.messenger = messenger;
	}

	@Override
	public void run() {
		for (String msg = messenger.take(); !"Done".equals(msg); msg = messenger.take()) {
			System.out.println("Message take: " + msg);
			try {
				Thread.sleep(ThreadLocalRandom.current().nextInt(1000));
			} catch (InterruptedException e) {
			}
		}
	}

	public Thread start() {
		Thread t = new Thread(this);
		t.start();
		return t;
	}
}

 

 

 

 

 

 

 

 

 

分享到:
评论

相关推荐

    Linux系统编程之线程同步

    所以,互斥锁实质上是操作系统提供的一把“建议锁”(又称“协同锁”),建议程序中有多线程访问共享资源的时候使用该机制。但,并没有强制限定。 因此,即使有了mutex,如果有线程不按规则来访问数据,依然会造成...

    java并发编程:线程基础

    线程的同步与阻塞: 引入多线程访问共享资源可能导致的问题,如竞态条件和数据不一致。介绍如何使用 synchronized 关键字来实现线程的同步和阻塞。 线程间通信: 详解线程间通信的方法,包括 wait、notify 和 ...

    Linux下多线程及多进程及同步与互斥编程详细介绍

    Linux下多线程及多进程及同步与互斥编程详细介绍

    Java多线程编程的优点和缺点

    竞态条件(Race Conditions):多个线程访问共享资源时可能引发竞态条件,导致数据不一致性和程序错误。 死锁(Deadlocks):多线程编程容易出现死锁,即多个线程相互等待对方释放资源的情况,导致程序无法继续执行...

    java多线程安全性基础介绍.pptx

    java多线程安全性基础介绍 线程安全 正确性 什么是线程安全性 原子性 竞态条件 i++ 读i ++ 值写回i 可见性 JMM 由于cpu和内存加载速度的差距,在两者之间增加了多级缓存导致,内存并不能直接对cpu可见。 ...

    java 多线程同步

    java.util.concurrent 包含许多线程安全、测试良好、高性能的并发构建块。不客气地说,创建 java.util....通过提供一组可靠的、高性能并发构建块,开发人员可以提高并发类的线程安全、可伸缩性、性能、可读性和可靠性。

    linux之线程同步一.doc

    线程同步在多线程编程中非常重要,因为它可以确保各个线程之间的数据安全和正确性。 以下是Linux中常见的线程同步机制: 1. 互斥锁(Mutex):互斥锁是一种用于保护共享资源的同步机制。当一个线程获得了一个互斥...

    linux之线程同步的概要介绍与分析

    在Linux操作系统中,线程同步是多线程编程中的一个核心概念,它确保了多个线程在访问共享资源时的正确性与一致性,避免了诸如数据竞争和竞态条件等问题。为了实现这一目标,Linux提供了一系列强大的线程同步机制和...

    多线程编程指南(系统描述了线程标准 线程同步 多线程编程原则 等)

    线程同步20 使用64 位体系结构20 2 基本线程编程23 线程库................................................................................................................................................. ...

    Linux多线程同步方式

    而操作系统对于多线程不会自动帮我们串行化,所以需要我们通过操作系统提供的同步方式api,结合自己的业务逻辑,利用多线程提高性能的同时,保证业务逻辑的正确性。一般而言,linux下同步方式主要有4种,原子锁,...

    VS2008开发的基于WinCE的网络服务器端和客户端程序多线程,线程同步,TCP IP网络通讯、阻塞式套接字发送数据与接收数据

    它支持多线程编程和线程同步,用于实现并发处理和保证数据的一致性。该程序使用TCP IP网络通信协议进行数据传输,通过阻塞式套接字实现数据的发送和接收。此外,还提供了VC++源码和固高嵌入式运动控制器的源代码。 ...

    winddows多线程程序设计

    Threads 不是新东西,但它借着 Windows 的庞大装机量初次广泛进入个人电脑 ...同步控制、多线程通讯、数据一致性……样样耗费你的心神,考验你专心致志的程度。读这本书,还请你武装一下自己的精神。

    C#线程锁介绍源码

    数 据库除了存储数据之外,还有一个重要的用途就是同步,数据库本身用了一套复杂的机制来保证数据的可靠和一致性,这就为我们节省了很多的精力。保证了数据源 头上的同步,我们多数的精力就可以集中在缓存等其他...

    深入理解Java内存模型??顺序一致性

    数据竞争与顺序一致性保证 ...  JMM对正确同步的多线程程序的内存一致性做了如下保证:  ● 如果程序是正确同步的,程序的执行将具有顺序一致性(sequentially consistent)–即程序的执行结果与该程序在顺序

    浅析c# 线程同步

    一致性维护 无线程干扰 C#锁定 使用 C# lock关键字同步执行程序。它用于为当前线程锁定,执行任务,然后释放锁定。它确保其他线程在执行完成之前不会中断执行。 下面,创建两个非同步和同步的例子。 C# 示例:非...

    深入理解java内存模型

    重排序对多线程的影响 顺序一致性 数据竞争与顺序一致性保证 顺序一致性内存模型 同步程序的顺序一致性效果 未同步程序的执行特性 VOLATILE volatile的特性 volatile写-读建立的happens before关系 volatile写-读的...

    Java理论与实践:描绘线程安全性

    并在被多个线程访问时,不管运行时环境执行这些线程有什么样的时序安排,它必须有如上所述的正确行为,并且在调用的代码中没有任何额外的同步。其效果就是,在所有线程看来,对于线程安全对象的操作是以固定的、全局...

    线程冲突 – Thread interference

    这种通信方式非常高效,但存在两个问题:线程冲突(thread interference) , 内存一致性错误(memory consistensy errors)。  解决这两种问题的方法是 线程同步(thread synchronization)。在介绍线程同步之前...

    Linux线程同步机制深度解析与实用指南.zip

    通过合理使用互斥锁、条件变量、读写锁和信号量等机制,可以确保多个线程之间的协调运行和数据一致性。在实际编程中,需要根据具体需求选择合适的同步机制并遵循最佳实践来避免潜在的问题。希望本文的介绍和指南能够...

    Java各种锁的使用方式及其对比

    Java中使用锁是为了在多线程程序中保证同步访问共享资源的正确性和一致性。在多线程环境下,多个线程可以同时访问共享资源,这可能导致数据的不一致性和错误的结果。例如,如果两个线程同时更新同一个变量,那么可能...

Global site tag (gtag.js) - Google Analytics