A.共享对象
一个线程在它的生命周期内都只会访问它自己的局部变量,那么他是无状态的,它永远是线程安全的,这是最好的状态,代码和非并发模式下没有什么不同。但是在高并发情况下,经常用同时访问一个共享数据,比如:
1.集合的CRU操作、一些符复合操作
2.某些关键资源的初始化,检查再运行(check-then-act)
如果不能很好的控制这些共享资源,那么就会有非线程安全的风险,进入预料之外的结果!
B.同步、可见性和原子性(atomicity)、重排
原子性:的操作在一定的临界区内一起完成,不会被其他线程的影响,一旦操作开始,那么他一定可以在可能发生的“上下文切换”之前执行完毕;但是对java来说,除了要保证原子性还要保证可见性
可见性:这是java内存模型决定的,虽然A线程原子的完成了变量的修改,但是B线程不一定看得到相应的修改,这时候就需要恰当的同步!
指令重排:
考虑下面的程序:
public class VisableTest {
private static boolean ready;
private static int number;
private static class ReaderThread extends Thread{
public void run(){
while(!ready)
Thread.yield();
System.out.println(number);
}
}
public static void main(String[] args){
new ReaderThread().start();
number=42;
ready=true;
}
}
VisableTest 可能永远保持循环,对于读线程来说,ready值可能永远不可见,甚至有可能会打印出0!这是因为“重排序(reordering)”,ready会在number之前写入,并且对读线程可见!
在没有同步的情况下,编译器、处理器,运行时安排操作的执行顺序可能完全出人意料。在没有进行适当同步的多线程程序中,尝试推断那些“必然”发生的内存动作,是不靠谱的!
C.内置锁
锁是保证原子性和可见性的有效工具,java内置的synchronized块就是内置锁的支持,每个java对象内部都有一个:
public void test(){
int i=12;
synchronized(this){
i++;
}
}
先将12压到栈顶
istore_1将栈顶存到局部变量区索引为1的位置
aload_0将索引0压到栈顶(一般索引为0的就是当前对象)
dup指令将当前栈顶的再拷贝一份压入栈顶,
astore_2将栈顶的引用存入索引为2的位置
monitorenter将当前栈顶的引用所指向的对象加锁
iinc 1,1将索引为1的元素+1
aload_2将索引为2的对象压入栈顶
Monitorexit将栈顶引用指向的对象释放锁
后面的指令确保在出现异常的时候锁的释放!
锁可以确保一个线程以可预见的方式看到另一个线程的修改,它不仅仅是关于同步和互斥的,也是关于内存可见的,为了保证所有线程都能看到共享的、可变变量的最新值,读取和写入线程必须使用公共的锁进行同步!
当前线程A得到锁以后,会在锁中记录下当前线程为占有者,当有其他线程调用该方法的时候,会将该线程放入锁对象的就绪(Ready)队列,当对象调用wait方法的时候,会将对应的线程放入等待(wait)队列!下面写了个简单的程序,发现会优先取等待队列中的线程:(发现notify也唤醒了所有等待的线程,why?)
public class WaitTest {
public static void main(String[] args) throws Exception{
WaitTest waitTest=new WaitTest();
waitTest.doo();
}
Public synchronized void ttttt(final ReaderThread tt,final int value){
new Thread(){
public void run(){
try {
if(value==2)
System.out.println("going to ready queue,"+
Thread.currentThread().getName());
tt.test(value);
} catch (InterruptedException e) {
}
}
}.start();
}
public synchronized void doo() throws InterruptedException{
final ReaderThread tt=new ReaderThread(1);
tt.start();
ttttt(1);
ttttt(1);
ttttt(0);
ttttt(2);
ttttt(2);
}
}
class ReaderThread extends Thread{
int i=0;
public ReaderThread(int i){
this.i=i;
}
public void run(){
try {
test(i);
} catch (InterruptedException e) {
}
}
public synchronized void test(int i) throws InterruptedException{
if(i==0){
System.out.println("i am .."+Thread.currentThread().getName());
TimeUnit.SECONDS.sleep(5);
this.notify();
}else if(i==1){
System.out.println("going to wait queue,"+
Thread.currentThread().getName());
this.wait();
System.out.println("i am weakup,"+
Thread.currentThread().getName());
}else if(i==2){
System.out.println("i am here,"+
Thread.currentThread().getName());
}
}
}
结果
going to wait queue,Thread-0
going to wait queue,Thread-1
going to wait queue,Thread-2
i am ..Thread-3
going to ready queue,Thread-4
going to ready queue,Thread-5
i am weakup,Thread-0
i am here,Thread-5
i am here,Thread-4
i am weakup,Thread-2
i am weakup,Thread-1
如果将notify换成notifyAll,会发现会先唤醒所有的等待线程,如果只是notify会唤醒一个等待线程,但是不知道为什么,到后面,其他的等待线程也都被唤醒了!
D.互斥性与可见性的保证
锁主要提供了两种特性:互斥性和可见性。互斥一次只允许一个线程持有某个特定的锁,因此可以使用该特性实现对共享数据的协调访问协议,这样,一次就只有一个线程能够使用该共享数据;可见性和java内存模型有关,她确保释放锁之前对共享数据作出的修改对于随后获得该锁的另一个线程是可见的。
对于一些“简单的变量”,可见性可以考虑使用volatile;一些变量自增操作的原子性,可以通过JUC的Atomic原子变量操作;
D.Volatile
Volatile相对域锁来说是更加轻量级的同步,使用volatile修饰的变量能够保证可见性,不过不能像锁一样保证原子性!
使用volatile修饰的时,不会将变量缓存也不会参与重排序,所以,读取一个volatile变量的时候,总会返回某一个线程写入的值
使用volatile的典型场景就是检查标记:
If(xxxx)
........
volatile 操作不会像锁一样造成阻塞,因此,在能够安全使用 volatile 的情况下,volatile 可以提供一些优于锁的可伸缩特性。如果读操作的次数要远远超过写操作,与锁相比,volatile 变量通常能够减少同步的性能开销。
F.ThreadLocal
另外一种防止共享资源上产生冲突的方式就是根除对变来那个的共享,使用线程本地存储的方式。当让ThreadLocal还有另外一个好处就是可以在多个方法之间传递变量,不用使用参数的形式。
ThreadLocal不是用来解决对象共享访问问题的,而主要是提供了保持对象的方法和避免参数传递的方便的对象访问方式。
一个ThreadLocal并非只能存放一个对象,网上有人讨论说:
SINCE1978 写道
我想知道如果多次new ThreadLocal并且调用其set方法的话、是否就和普通hashmap一样后set进去的会覆盖先set进去的?这样的话ThreadLocal只能植入一个资源喽?这肯定不对,否则还用ThreadLocalMap这个自定义哈希表干什么,那么如何区分一个线程当中不同方法或不同类set进去的资源?并正确set和get??
每个ThreadLocal当然只能放一个对象。要是需要放其他的对象,就再new 一个新的ThreadLocal出来,这个新的ThreadLocal作为key,需要放的对象作为value,放在ThreadLocalMap中。。。。
甚至还有人通过某些方式提供了一种一个ThreadLocal存放更丰富的对象比如Map,不用实例化太多thread
local的方法,但是看了源码之后,我们会发现
初始化多次TheadLocal并没有多大的问题,也没有什么资源的浪费,一些人的误解可能是因为对ThreadLocalMap的误解:以为是一个ThreadLocal对应一个ThreadLocalMap,其实,是一个Thread对应一个ThreadLocalMap,所以在一个线程中创建多个ThredLocal实例的开销只有:set的时候,要将threadLocalHashCode
来计算哈希值,并将其放到线程实例唯一的ThreadLocalMap中,或者说是哈希函数计算后,放到数组中,其他的开销,都在第一次set的时候就做了,就是创建一个线程唯一的ThreadLocalMap。
F.协作 wait和notify
当使用多个线程来同时运行多个任务的时候,可以使用锁来同步两个任务的行为,这样保证不会相互干扰。但是,有些任务可能需要线程之间协作解决,这不再是彼此之间的干涉,而是彼此之间的协调!
让这些线程协作,关键就是握手,这可以通过基础特性:互斥,可以确保只有一个任务可以响应某个信号,在互斥的基础上,还有一个途径,可以将自己挂起,直到外部条件发生变化!
这可以用Object的wait和notify方法来安全的实现,另外,JAVA5还提供了具有await和signal方法的Condition对象!
Wait可以让你等待某个条件变化,这个变化由另一个任务来改变。它和sleep有两个显著的不同:
1.Wait期间锁是释放的
2.可以通过notify/notifyAll或者时间到期,让wait恢复执行
前面已经说过,获得锁有一个等待区域,每一个同步锁lock下面都挂了几个线程队列,包括就绪(Ready)队列,等待(Waiting)队列等。当线程A因为得不到同步锁lock,从而进入的是lock.ReadyQueue(就绪队列),一旦同步锁不被占用,JVM将自动运行就绪队列中的线程而不需要任何notify()的操作。但是当线程A被wait()了,那么将进入lock.WaitingQuene(等待队列),同时如果占据的同步锁也会放弃。而此时如果同步锁不唤醒等待队列中的进程(lock.notify()),这些进程将永远不会得到运行的机会。
- 大小: 18.6 KB
分享到:
相关推荐
线程的同步与阻塞: 引入多线程访问共享资源可能导致的问题,如竞态条件和数据不一致。介绍如何使用 synchronized 关键字来实现线程的同步和阻塞。 线程间通信: 详解线程间通信的方法,包括 wait、notify 和 ...
一个进程可以包含多个线程,这些线程共享进程的资源。 并发与并行:并发是指多个任务在逻辑上同时发生,而并行是指多个任务在物理上同时发生(例如在多核处理器上)。 同步(Synchronization):同步是控制多个线程...
保护“共享数据” 低级并发工具 原子变量 锁(内部锁和显式锁) 线程安全容器 同步容器 并发容器 阻塞队列 高级线程协作工具 信号量 闭锁 关卡 fork-join Executor部分 Executor基础...
启用太多的线程,就有搞垮机器的可能认识Java里的线程新启线程的方式三种怎么样才能让Java里的线程安全停止工作呢线程自然终止:自然执行完或抛出未处理异常sto
当多个线程访问同一个对象时,如果不用考虑这些线程在运行环境下的调度和交替运行,也不需要进行额外的同步,或者在调用方进行任何其他的协调操作,调用这个对象的行为都可以获取正确的结果,那么这个对象时线程的 ...
譬如可以将问题划分为子问题并将子问题交给不同的线程进行处理,如果这些线程需要共享一些争用资源,那么通常对这些争用资源的访问(读或者写操作)是需要进行互斥操作的(解决并发问题);如果这些线程在某些时候需要...
互斥(保证同一时刻只允许一个线程访问共享资源)可重入锁 如何学习 跳出来,看全景,站在模型角度看问题(避免盲人摸象) 例如:synchronized、wait()、notify()不过是操作系统领域里管程模型的一种实现而已 钻进去...
事件处理器与访问共享状态的其他代码都要采取线程安全的方式实现 框架通过在框架线程中调用应用程序代码将并发性引入应用程序,因此对线程安全的需求在整个应用程序中都需要考虑 基础知识 线程安全性 ...
服务器端技术:使用Java技术实现服务器端的业务逻辑和会议管理功能,如多线程编程、数据持久化等。 安全性考虑: 数据加密:对视频、音频和聊天数据进行加密处理,确保数据的机密性和完整性。 身份认证:采用安全的...
本书深入浅出地介绍了Java线程和并发,是一本完美的Java并发参考手册。书中从并发性和线程安全性的基本概念出发,介绍了如何使用类库提供的基本并发构建块,用于避免并发危险、构造线程安全的类及验证线程安全的规则...
即就是同一时刻只允许一个线程访问共享资源的问题。 - 同步(Synchronization):两个或两个以上的进程或线程在运行过程中协同步调,按预定的先后次序运行。即就是线程之间如何通信、协作的问题。 针对对于这两大核心...
使用适当的对象锁和/或等待通知信号来协作处理共享对象。 描述 在本次作业中,您将实现自己的 在本作业中,您将实现您自己的互联网聊天协议——ChattyChatChat 协议 (CCC)。 该协议控制单个 ChattyChatChat 服务器...
在并发编程领域,有两大核心问题:一个是互斥,即同一时刻只允许一个线程访问共享资源;另一个是同步,即线程之间如何通信、协作。 主要原因是,对于多线程实现实现并发,一直以来,多线程都存在2个问题: ● 线程...
Java中线程问问题描述,多线程的优势...1、进程之间不能共享内存,线程之间共享内存更容易,多线程可协作完成进程工作; 2、创建进程进行资源分配的代价较创建线程要大得多,所以多线程在高并发环境中效率更高。
使用适当的对象锁和/或等待通知信号来协作处理共享对象。 描述 在本次作业中,您将实现自己的 在本作业中,您将实现您自己的互联网聊天协议——ChattyChatChat 协议 (CCC)。 该协议控制单个 ChattyChatChat 服务器...
第一章 线程基础、线程之间的共享和协作 3 一、基础概念 3 1. 什么是进程和线程 3 2. CPU核心数和线程数的关系 3 3. 澄清并行和并发 5 4. 多线程程序需要注意事项 6 二、认识Java里的线程 7 1. Java程序天生就是多...
使用适当的对象锁和/或等待通知信号来协作处理共享对象。 描述 在本次作业中,您将实现自己的 在本作业中,您将实现您自己的互联网聊天协议——ChattyChatChat 协议 (CCC)。 该协议控制单个 ChattyChatChat 服务器...
使用适当的对象锁和/或等待通知信号来协作处理共享对象。 描述 在本次作业中,您将实现自己的 在本作业中,您将实现您自己的互联网聊天协议——ChattyChatChat 协议 (CCC)。 该协议控制单个 ChattyChatChat 服务器...