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
分享到:
相关推荐
Java线程是并发编程的核心部分,它允许程序同时执行多个任务,从而提高系统效率和响应速度。在这个"java线程1-10-720p版本"的学习资源中,我们将会深入理解Java线程的基本概念、创建方式、以及如何进行有效管理。 ...
### Java多线程知识点总结及企业真题解析 #### 一、知识点总结 ##### (1)多线程相关概念 1. **程序、进程和线程的区分**: - **程序**:为了完成特定的任务而编写的指令集合。它是静态的概念。 - **进程**:...
2. **创建和启动Java线程** - **继承Thread类**:创建一个新的类,继承自`java.lang.Thread`,重写`run()`方法,然后创建该类的实例并调用`start()`方法来启动线程。 - **实现Runnable接口**:创建一个实现了`...
以下是一些关于Java线程的常见面试知识点,这些内容可能出现在线程.doc文档中: 1. **线程的创建方式**: - 实现`Runnable`接口:创建一个类实现`Runnable`接口,并重写`run()`方法,然后将实例传递给`Thread`类的...
#### 九、Java线程:并发协作-生产者消费者模型 - 生产者消费者模型是一种经典的多线程设计模式,用于解决线程间的协作问题。通常通过队列或缓冲区来实现。 #### 十、Java线程:并发协作-死锁 - 死锁是多个线程...
2. **解决线程安全问题**:由于两个线程需要共享同一个计数器变量`number`,因此需要对计数器的访问进行同步控制。 3. **等待和唤醒**:为了让两个线程能够交替打印数字,需要利用`wait()`和`notify()`方法。 **...
总的来说,Java的线程通信机制为多线程环境下的协作提供了基础。理解并正确使用`wait()`、`notify()`和`notifyAll()`,以及如何通过共享对象进行通信,是编写高效并发程序的关键。在实际编程中,应确保避免竞态条件...
- 同步是为了确保线程间正确地共享数据,避免数据竞争,Java提供了`synchronized`, `wait()`, `notify()`, `notifyAll()`等机制。 6. **守护线程 (Daemon)**: - 守护线程是支持其他非守护线程运行的后台线程,当...
1. **线程安全**:由于多线程环境下可能存在数据竞争,所以在访问共享资源(如数据库连接)时,需要确保线程安全。可以使用`synchronized`关键字或者`Lock`来同步访问。 2. **事务管理**:在多线程环境中,可能需要...
#### 九、Java线程:并发协作 - **生产者消费者模型** 一种经典的并发模式,其中“生产者”线程负责生产数据,“消费者”线程负责消费数据。这种模型通常通过队列结构来实现。 - **死锁** 死锁发生在多个线程...
Java线程与多线程是Java开发中的核心概念,对于任何Java开发者来说,理解和掌握这部分内容至关重要。在Java中,线程是程序执行的基本单元,它允许程序同时执行多个任务,提高了程序的运行效率和响应速度。本教程将...
- **同步的重要性**:当多个线程访问同一个共享资源时,为了防止数据竞争和不一致的情况发生,需要采用同步机制。 - **同步机制**:Java中常用的同步机制包括synchronized关键字、显式锁(Lock)、原子变量...
在Java多线程编程中,线程安全问题是非常关键的概念,它涉及到多个线程访问共享资源时可能出现的数据不一致或异常情况。本题主要通过两个练习题来加深对线程安全的理解。 ### 练习题1:新年倒计时 #### 题目描述 ...
线程同步机制包括`synchronized`、`wait()`、`notify()` 和 `notifyAll()` 等,它们用于控制线程间的协作和通信,防止数据竞争和死锁的发生。 此外,Java还提供了线程池(ThreadPool)来管理线程的生命周期,通过...
#### 十、Java线程:并发协作-生产者消费者模型 生产者消费者模型是一种经典的线程协作模式,用于解决多线程间的通信问题。在这种模型中,生产者线程负责生成数据,消费者线程负责处理这些数据。 #### 十一、Java...
这个过程需要确保线程安全,避免数据竞争和其他并发问题。 在Java中,我们可以使用`java.util.concurrent`包下的`BlockingQueue`接口来实现生产者消费者模式。`BlockingQueue`提供了线程安全的队列操作,如添加...
#### 九、Java线程:并发协作-生产者消费者模型 - **生产者消费者模型**: - 是一种经典的线程协作模型,用来解决生产者和消费者之间的数据交换问题。 - 生产者负责生产数据,消费者负责消费数据,两者通过共享...
- **保护共享数据:** 当多个线程共享内存中的数据时,互斥机制能够确保这些数据在任何时刻都是正确的。 **实现方式:** - **锁(Locks):** 最常见的互斥手段之一,包括可重入锁、读写锁等。 - **同步代码块:** ...
下面将围绕“Java线程”这一主题展开详细的介绍与解释。 ### Java线程基础 在Java语言中,线程是程序执行流的基本单元。一个标准的Java应用程序至少会有一个线程,即主线程,用于执行程序的主要逻辑。通过创建多个...