`

多线程, 并发编程知识总结

    博客分类:
  • Java
阅读更多
引子
一点体会:对并发和多线程的学习理解, 最好多作几个例子跟踪调试实战一把, 可能会理解的更深刻些. 比如像我这样

Java内存模型

共享数据保存在主存储器中, 计算机将数据中主存储器读到寄存器中, 然后进行操作, 一般情况下, 对寄存器的操作速度要比主存储器要快.

Lock(锁机制)
...

synchronized
当线程进入synchronized方法或者代码块的时候, 它必须重新将数据从主存储器中加载到当前线程的寄存器中.并对同步的对象加锁,从而阻塞其他线程的进入, 线程在离开之前再将寄存器中的数据同步到主存储器中并释放锁.

synchronized只会对共享的内容有效, 如果不存在同时对共享内容进行操作, 则同步毫无意义.比如下面的测试代码:
public class SyncTest {
	public void syncMethod1(SyncDomain domain) {
		synchronized (domain) {
			System.out.println(domain.getStr());
		}
	}

	public static void main(String[] args) {
		final SyncTest test = new SyncTest();
		new Thread() {
			@Override
			public void run() {
				SyncDomain domain = new SyncDomain();
				domain.setStr("t1");
				test.syncMethod1(domain);
			}
		}.start();

		new Thread() {
			@Override
			public void run() {
				SyncDomain domain = new SyncDomain();
				domain.setStr("t2");
				test.syncMethod1(domain);
			}
		}.start();
	}

这里虽然使用到了synchronized块, 但是该方法不存在对公共的内容进行加锁, 而锁住的是两个不同的domain, 如果锁定的对象改成this则就回达到我们期望的结果

synchronized修饰的方法和代码块, 一次只能有一个线程访问, 而一个类的多个方法被synchronized修饰, 则这些方法只能由一个线程访问, 但是这里有一个区别, 静态synchronized方法之间只允许一个线程访问, 非静态的synchronized方法之间只允许一个线程访问, 静态和非静态方法之间不存在互斥的情况.比如:
public synchronized static void method1(){...}
public synchronized void method2(){...}

这两个方法之间不会互为阻塞.

在可能的情况下,一般尽量将要同步的代码最小化, 这样可以达到线程的阻塞最小化.
对于下面两个同步方法:
public synchronized void method1(){...}
public synchronized void method2(){...}

如果线程a执行到method1()方法, 而线程b同时执行到method2()方法则会被阻塞, 直到线程a退出method1()方法为止.

而对于下面的测试则是另外一种情况:
public class SyncBlockAndNonBlockMethodTest {
	public synchronized  void syncMethod1(SyncDomain domain) {
		System.out.println(domain.getStr() + ": invoke before");
	}
	public void syncMethod2(SyncDomain domain) {
		// 如果同步块锁定的是this, 那么执行syncMethod2方法时, 访问syncMethod1方法的线程将被阻塞
		// 而如果同步块锁定的是domain, 那么两个同步方法互不影响
		synchronized(domain) {
			System.out.println(domain.getStr() + ": invoke before");
		}
	}

	public static void main(String[] args) {

		final SyncBlockAndNonBlockMethodTest test = new SyncBlockAndNonBlockMethodTest();
		final SyncDomain domain = new SyncDomain();
		new Thread() {
			@Override
			public void run() {
				// 断点1
				domain.setStr("t1");
				test.syncMethod1(domain);
			}
		}.start();

		new Thread() {
			@Override
			public void run() {
				//SyncDomain domain = new SyncDomain();
				// 断点2
				domain.setStr("t2");
				test.syncMethod2(domain);
			}
		}.start();
	}

}

这主要是因为如果synchronized块中是domain的话, 那么synchronized方法锁住的是this对象, synchronized锁住的是domain对象, 因此两个synchronized互不影响.

volatile
volatile是一种轻量级的synchronized, synchronized是对代码块和方法加锁进行同步, 而volatile修饰的共享变量能保证在多线程状态下做到同步, 通常情况下多线程在操作共享变量的时候, 都会在工作内存中将主存中的共享变量复制一份, 因此在一个线程中修改了共享变量, 可能还没有更新到主存中, 而另一个线程又访问了主存中的共享变量, 这样将导致另一个线程使用过期的数据, 使用volatile修饰的共享变量则能保证线程操作的都是主存中的共享变量, 工作变量不会在工作内存拷贝一份再处理, 这样就保证了多个线程访问到的共享变量时刻都是最新的, 因为都在主存中操作volatile共享变量, 因此保证对共享变量的修改是"原子"级的操作(不存在工作内存和主存之间的同步更新), 因此实现了线程安全. 比如说有一个共享变量i, 然后在一个方法中有这样的操作i++, 它实际上是一个包含了三个操作的复合操作:读, 改, 写.  如果不对i不加volatile, i又在另外一个线程中被修改, 那么可能出现i++操作读的时候是另外一个线程未更新前的值, 改的同时另一个线程的工作内存中的变量与主存在做同步更新, 最后写的时候就会将另外一个线程的修改覆盖掉. 从上面的例子中可以看出, 这种情况需要较高的压力与并发情况下, 才会出现. 同时这个例子即使使用volatile也无法完全保证线程安全, 必须将i++操作使用synchronized包装成一个原子操作, 或者使用jdk1.5的atomic原子包中的类实现原子操作.

volatile 变量仅能被安全地用在单一的载入或存储操作。这个限制导致volatile变量的使用是不常见的。

理论上每一个线程都有自己的寄存器来存放操作数据. 而使用volatile, 则可以保存每个线程操作的数据保存在主存储器中, 达到多个线程之间能够共享

原子变量(AtomicLong, AtomicInteger, AtomicReference)
J2SE 5.0提供了一组atomic class来帮助我们简化同步处理。基本工作原理是使用了同步synchronized的方法实现了对一个long, integer, 对象的增、减、赋值(更新)操作. 比如对于++运算符AtomicInteger可以将它持有的integer 能够atomic 地递增。在需要访问两个或两个以上 atomic变量的程序代码(或者是对单一的atomic变量执行两个或两个以上的操作)通常都需要被synchronize以便两者的操作能够被当作是一个atomic的单元。

对array atomic变量来说,一次只有一个索引变量可以变动,并没有功能可以对整个array做atomic化的变动。

关于Atomic的几个方法
getAndSet() : 设置新值,返回旧值.
compareAndSet(expectedValue, newValue) : 如果当前值(current value)等于期待的值(expectedValue), 则原子地更新指定值为新值(newValue), 如果更新成功,返回true, 否则返回false, 换句话可以这样说: 将原子变量设置为新的值, 但是如果从我上次看到的这个变量之后到现在被其他线程修改了(和我期望看到的值不符), 那么更新失败

从effective java (2)中拿来的一个关于AtomicReference的一个例子:
public class AtomicTest {
	private int x, y;

	private enum State {
		NEW, INITIALIZING, INITIALIZED
	};

	private final AtomicReference<State> init = new AtomicReference<State>(State.NEW);
	
	public AtomicTest() {
	}
	
	public AtomicTest(int x, int y) {
		initialize(x, y);
	}

	private void initialize(int x, int y) {
		if (!init.compareAndSet(State.NEW, State.INITIALIZING)) {
			throw new IllegalStateException("initialize is error");
		}
		this.x = x;
		this.y = y;
		init.set(State.INITIALIZED);
	}

	public int getX() {
		checkInit();
		return x;
	}

	public int getY() {
		checkInit();
		return y;
	}
	
	private void checkInit() {
		if (init.get() == State.INITIALIZED) {
			throw new IllegalStateException("uninitialized");
		}
	}
	
}

上面的例子比较容易懂, 不过貌似没什么价值, 而在实际的应用中, 我们一般采用下面的方式来使用atomic class:
public class CounterTest {
	AtomicInteger counter = new AtomicInteger(0);

	public int count() {
		int result;
		boolean flag;
		do {
			result = counter.get();
			// 断点
			// 单线程下, compareAndSet返回永远为true,
			// 多线程下, 在与result进行compare时, counter可能被其他线程set了新值, 这时需要重新再取一遍再比较,
			// 如果还是没有拿到最新的值, 则一直循环下去, 直到拿到最新的那个值
			flag = counter.compareAndSet(result, result + 1);
		} while (!flag);

		return result;
	}

	public static void main(String[] args) {
		final CounterTest c = new CounterTest();
		new Thread() {
			@Override
			public void run() {
				c.count();
			}
		}.start();

		new Thread() {
			@Override
			public void run() {
				c.count();
			}
		}.start();

		new Thread() {
			@Override
			public void run() {
				c.count();
			}
		}.start();
	}
}


类似i++这样的"读-改-写"复合操作(在一个操作序列中, 后一个操作依赖前一次操作的结果), 在多线程并发处理的时候会出现问题, 因为可能一个线程修改了变量, 而另一个线程没有察觉到这样变化, 当使用原子变量之后, 则将一系列的复合操作合并为一个原子操作,从而避免这种问题, i++=>i.incrementAndGet()
原子变量只能保证对一个变量的操作是原子的, 如果有多个原子变量之间存在依赖的复合操作, 也不可能是安全的, 另外一种情况是要将更多的复合操作作为一个原子操作, 则需要使用synchronized将要作为原子操作的语句包围起来. 因为涉及到可变的共享变量(类实例成员变量)才会涉及到同步, 否则不必使用synchronized

wait, notify, notifyAll, sleep, yield
wait暂停被当前同步锁当前线程的执行, 同时线程会释放其同步锁, 使用同一锁的线程将有机会通过notify或notifyAll被唤醒
notify唤醒当前暂停的线程进入执行队列等待执行
notifyAll会唤醒多个线程进入执行队列, 这些线程根据最终的竞争结果被继续执行
sleep暂停当前线程的执行, 但是不释放同步锁, 这样使用同一锁的线程将被阻塞
yield会将当前线程暂时让位一小段时间,让其它的线程有机会运行,过了这段时间后,该线程继承运行。上述功能也可以用Thread.sleep()方法实现。简单的说就是在线程执行过程中sleep(0)一下, 让其他等待的线程继续运行

线程安全
编写线程安全的代码,本质上就是管理对状态(state)的访问,而且通常都是共享的、可变的状态。这里的状态就是对象的变量(静态变量和实例变量)
线程安全的前提是该变量是否被多个线程访问, 保证对象的线程安全性需要使用同步来协调对其可变状态的访问;若是做不到这一点,就会导致脏数据和其他不可预期的后果。无论何时,只要有多于一个的线程访问给定的状态变量,而且其中某个线程会写入该变量,此时必须使用同步来协调线程对该变量的访问。Java中首要的同步机制是synchronized关键字,它提供了独占锁。除此之外,术语“同步”还包括volatile变量,显示锁和原子变量的使用。

在没有正确同步的情况下,如果多个线程访问了同一个变量,你的程序就存在隐患。有3种方法修复它:
l 不要跨线程共享变量;
l 使状态变量为不可变的;或者
l 在任何访问状态变量的时候使用同步。

当多个线程访问一个类时,如果不用考虑这些线程在运行时环境下的调度和交替执行,并且不需要额外的同步及在调用方代码不必作其他的协调,这个类的行为仍然是正确的,那么称这个类是线程安全的。

如果共享变量在多个方法中访问到, 当在多线程环境下, 仅仅对其中一个方法使用synchronized是不够的, 为了保证线程安全, 必须对多个方法都使用synchronized, 具体做法可以参看vector, vector相对于list来说, 是线程安全的, 它的所有公共的方法都是synchronized, 这也是synchronized的另一个作用, 保证所有方法使用的共享变量不是过期数据.

util.concurrent
map的数据结构, map内部是一个存放单向链接的Entry元素的一维数组(map称之为bucket), 首先根据每一个元素的key的hashcode按照一维数组的length取模, 得到的结果就是该元素存放在这个数组的位置, 存放在这个数组的元素可能有多个, 这些元素将通过单向链表的方式进行关联
HashTable是线程安全的, 因此它的所有方法都是synchronized的, 而HashMap是非线程安全的. 但是传统的HashTable在高性能的环境下性能很差, 因为每次只能有一个线程对HashTable进行读写, 而针对大量读, 少量写的应用场景来说, 只能一个线程能读将严重影响性能, 为了解决这个问题, 将原来对整个Map锁定改成对Map中的一个或几个bucket锁定(即减小锁的粒度)来实现能有多个线程能同时进行读,写操作. 对于大量的读操作(get), 少量的写操作(set), concurrent采用了copy-on-write策略, 即对于写操作, 将后台的数组复制一份, 然后对副本进行写操作, 完成之后, 替换原来的数组, 这样可以不影响读操作. 其实现有CopyOnWriteArrayList和CopyOnWriteArraySet
线程池是为发挥多线程的优点(并发),避免多线程的缺点(创建和销毁的时空开销)而出现的. 一个比较简单的线程池至少应包含线程池管理器、工作线程、任务队列、任务接口等部分。其中线程池管理器(ThreadPool Manager)的作用是创建、销毁并管理线程池,将工作线程放入线程池中;工作线程是一个可以循环执行任务的线程,在没有任务时进行等待;任务队列的作 用是提供一种缓冲机制,将没有处理的任务放在任务队列中;任务接口是每个任务必须实现的接口,主要用来规定任务的入口、任务执行完后的收尾工作、任务的执 行状态等,工作线程通过该接口调度任务的执行。
分享到:
评论

相关推荐

    Java_多线程与并发编程总结.doc

    总之,Java的多线程和并发编程是一个复杂而重要的主题,它涉及到操作系统原理、JVM行为、线程管理、同步机制等多个方面,熟练掌握这些知识对于开发高效、可靠的Java应用程序至关重要。通过理解线程的工作原理和使用...

    Netty多线程并发编程

    Netty多线程并发编程知识点总结 Netty多线程并发编程是指在Netty框架中使用多线程技术来实现高性能、高并发的网络编程。下面是关于Netty多线程并发编程的知识点总结: 一、 JAVA 内存模型与多线程编程 在Java中,...

    Java 多线程与并发编程总结.doc

    Java多线程与并发编程是Java...总之,Java多线程与并发编程是Java程序员必须掌握的核心技能,它涉及到操作系统原理、JVM内部机制以及Java提供的各种并发工具,熟练掌握这些知识对于编写高效、稳定的并发程序至关重要。

    汪文君高并发编程实战视频资源下载.txt

    │ 高并发编程第二阶段04讲、多线程的休息室WaitSet详细介绍与知识点总结.mp4 │ 高并发编程第二阶段05讲、一个解释volatile关键字作用最好的例子.mp4 │ 高并发编程第二阶段06讲、Java内存模型以及CPU缓存不一致...

    Java 多线程与并发编程总结

    Java多线程与并发编程是Java开发中不可或缺的一部分,它涉及到操作系统的基础概念、JVM的工作原理以及Java语言特性。多线程允许程序同时执行多个任务,以充分利用CPU资源,提高系统的效率和响应性。 首先,理解多...

    并发服务器-多线程服务器详解

    一种典型的多线程并发服务器架构如下: - **主监听线程**:负责接收客户端连接请求。 - **工作线程池**:由多个工作线程组成,负责处理具体的客户端请求。 **3. 示例代码片段** 下面是一个简单的多线程服务器示例...

    Java多线程、并发编程知识点总结.pdf

    Java多线程编程是Java语言的核心特性之一,...这些知识点是Java多线程编程的精髓,涵盖了从创建线程到线程间协作、线程安全与死锁处理等多个方面。掌握这些知识点对于编写高效、稳定、可维护的Java多线程程序至关重要。

    Linux多线程编程知识点总结(C语言)(csdn)————程序.pdf

    本文主要围绕Linux下C语言实现的多线程编程进行知识点总结,涉及线程与进程的区别、多线程的优势、Pthreads API以及线程安全和死锁预防。 首先,线程和进程是操作系统中两种基本的执行单元。线程是进程内部的执行流...

    JAVA多线程编程技术PDF

    这份“JAVA多线程编程技术PDF”是学习和掌握这一领域的经典资料,涵盖了多线程的全部知识点。 首先,多线程的核心概念包括线程的创建与启动。在Java中,可以通过实现Runnable接口或继承Thread类来创建线程。创建后...

    Python多进程并发与多线程并发编程实例总结

    Python多进程并发与多...以上知识点总结了Python在多进程并发和多线程并发编程方面的概念、优势与劣势、实现方法、注意事项等内容。掌握了这些知识点,可以帮助开发人员在实际应用中设计出更加高效和稳定的并发程序。

    Java多线程、并发编程知识点总结[归类].pdf

    以下是对Java多线程、并发编程的一些关键知识点的详细阐述: 1. **线程创建**: - 接口方式:通过实现`Runnable`接口,然后通过`Thread`类构造器传入`Runnable`实例来创建线程。 - 类方式:直接继承`Thread`类并...

    汪文君高并发编程实战视频资源全集

    │ 高并发编程第二阶段04讲、多线程的休息室WaitSet详细介绍与知识点总结.mp4 │ 高并发编程第二阶段05讲、一个解释volatile关键字作用最好的例子.mp4 │ 高并发编程第二阶段06讲、Java内存模型以及CPU缓存不一致...

    多线程并发服务器(毕业设计)

    在IT领域,多线程并发服务器是网络编程中的一个重要概念,尤其在高并发场景下,如网站、在线游戏和实时数据处理系统等。这个毕业设计的主题是“多线程并发服务器”,采用C++语言实现,这表明我们将在讨论中涵盖C++...

    Java+多线程与并发编程总结[借鉴].pdf

    Java多线程与并发编程是软件开发中的重要领域,它涉及到如何高效地利用计算机资源,尤其是CPU,使得多个任务能够同时或并发地执行。在Java中,多线程的运用可以提升程序的响应速度和执行效率,特别是在处理大量并发...

    vc多线程编程简单介绍vc多线程编程简单介绍vc多线程编程简单介绍

    总结,VC多线程编程涉及线程的创建、同步、通信、生命周期管理等多个方面,理解并熟练掌握这些知识点,能够帮助开发者构建高效、稳定的多线程应用。在实际开发中,还需要考虑线程池、异步操作等高级特性,以优化系统...

    Java多线程知识点总结

    Java多线程是Java编程语言中一个非常重要的概念...理解这些知识点对于解决并发编程中的问题,比如资源竞争、死锁、线程安全性等问题,至关重要。在实际开发中,正确地应用多线程机制可以显著提升程序的性能和用户体验。

    Java并发编程实践多线程

    《Java并发编程实践》是一本深入探讨Java多线程编程的权威著作,它详细阐述了在并发环境下如何设计和实现高效、可靠的程序。这本书涵盖了Java并发编程的核心概念、工具和最佳实践,对于想要提升Java并发编程技能的...

    Linux多线程 C语言编程关于多线程

    ### Linux多线程 C语言编程知识点总结 #### 一、多线程与多进程编程概述 **多线程**的概念可以追溯到20世纪60年代,然而直到80年代中期,这一机制才被正式引入到Unix系统中。随着计算机硬件的发展及软件需求的增加...

Global site tag (gtag.js) - Google Analytics