您的当前位置:首页正文

JAVA基础3-JAVA线程学习笔记(1)

2024-11-25 来源:个人技术集锦

1.1 基础类

1.1.1 Runnable

定义一个接口,里面只有run一个方法。

1.1.2 Thread

实现Runnable接口,并实现包括wait、notify、yield、sleep等方法。

1.1.3 Runable与Thread比较

实现Runnable接口比继承Thread类所具有的优势:
1):适合多个相同的程序代码的线程去处理同一个资源
2):可以避免java中的单继承的限制
3):增加程序的健壮性,代码可以被多个线程共享,代码和数据独立
4):线程池只能放入实现Runable或callable类线程,不能直接放入继承Thread的类

1.2 线程状态图

1.3 基础方法

start:线程启动方法
wait:必须要与synchronized(Obj)一起使用,线程处于等待队列,除非有notify唤醒。同时释放锁定资源
notify:必须要与synchronized(Obj)一起使用,线程唤醒。
yield:现场处于可执行状态,让出执行权,给予优先级相等的线程,有可能让出后又再次获得cpu权限,所以具有不确定性。
sleep:现场处于阻塞或者睡眠状态,但是给予一定时间后继续执行。没有释放对象锁的控制。
join:等待其他线程终止。在当前线程中调用另一个线程的join()方法,则当前线程转入阻塞状态,直到另一个进程运行结束,当前线程再由阻塞转为就绪状态。
setPriority:设定优先级,有以下3个级别:
MIN_PRIORITY = 1
   NORM_PRIORITY = 5
MAX_PRIORITY = 10

1.4 线程基本概念

1.4.1 竞态条件

当两个线程竞争同一资源时,如果对资源的访问顺序敏感,就称存在竞态条件。

1.4.2 临界区

导致竞态条件发生的代码区称作临界区。

1.4.3 丢失的信号

如果一个线程先于被通知线程调用wait()前调用了notify(),等待的线程将错过这个信号。使用保存信号变量来解决。

1.4.4 虚假唤醒

由于莫名其妙的原因,线程有可能在没有调用过notify()和notifyAll()的情况下醒来。这就是所谓的假唤醒。通过自旋锁解决。

1.4.5 自旋锁

为了防止假唤醒,保存信号的成员变量将在一个while循环里接受检查,而不是在if表达式里。这样的一个while循环叫做自旋锁。用于处理假唤醒,但是CPU消耗会很大。

1.5 Java内存模型

Java间的内存操作由一下8种。
Lock:作用于主内存,标示一个变量为一个线程独占。
Unlock:作用于主内存,释放一个变量的独占,让其他线程可以使用。
Read:作用于主内存,将变量从主内存写入到传输通道。
Load:作用于工作内存,将变量从传输通道放到工作内存中。
Use:作用于工作内存,将工作内存加载到执行引擎。
Assign:作用于工作内存,将执行引擎变量存储到工作内存。
Store:作用于工作内存,将工作内存的变量写入到传输通道。
Write:作用于主内存,将传输通道变量写入主内存。

1.6 Java内存模型的特性

1.6.1 原子性

原子性代表一次性所有操作要么全部成功,要么全部不成功。Java的基本类型在上面提到的8个操作都具有原子性。除了long和double类型(不过这2个在很多虚拟机上还是具有原子性)。

1.6.2 可见性

当一个线程修改主内存的共享变量,其它线程能够立即得知这个修改。Java里面都实现这种可见性。但是Java里面的volatile保证每次修改都写到主内存,每次读取都刷新主内存。普通变量则不能保证这一点。除了volatile外,synchronized和final也能保证可见性。

1.6.3 有序性

线程内的所有操作都是有序的,但是相对于多线程来说,之间的操作就无序了。

1.7 线程安全实现方法

1.7.1 互斥同步(悲观锁)

使用类似锁机制,保证共享数据在同一时刻只被一个线程使用。Java中使用临界区、互斥量和信号量来实现。其中synchronized和锁ReentrantLock模式就是采用互斥模式。

1.7.2 非阻塞同步(乐观锁)

定义:先进行操作,如果未有进程争用共享数据,则操作成功;如果共享数据有争用,产生冲突,在采用其他的补偿措施(比如不断重试直到成功)。好处就是不需要将线程挂起,减少切换线程带来的消耗。
可以看出,检查冲突和操作是需要原子性保证的,所以这种模式需要通过硬件来实现,目前处理器使用如下几种:
测试并设置(Test-and-Set)
获取并增加(Fetch-and-Increment)
交换(Swap、)
比较并交换(Compare-and-Swap,简称CAS)。
加载链接/条件存储(Load-Linked/Store-Conditinal,简称LL/SC)。
前三种已经存在,后面两种在Java1.5之后才使用。其中CAS操作在java中的sun.misc.Unsafe类里面的compareAndSwapInt方法和compareAndSwapLong()等几个方法包装提供。包括AtomicInteger等类也具有同样功能。

1.7.3 无同步方案

不存在数据共享的情况,就不需要同步。
可重写入代码(纯代码)和线程本地存储。
其中java实现的线程本地存储类有TreadLocal和InheritableThreadLocal就实现这样功能。

1.8 锁的基本概念

1.8.1 死锁

线程1需要A和B个资源,线程2需要A和B资源。当线程1获取A资源,等待获取B资源,线程2获取B资源,等待获取A资源时,就造成死锁。
解决方法:1.加锁顺序 2.加锁限时 3.死锁监测。

1.8.2 线程饥饿

如果一个线程因为CPU时间全部被其他线程抢走而得不到CPU运行时间,这种状态被称之为“饥饿”。
Java中导致饥饿的原因:

1.8.3 线程公平锁

在Java中实现公平性方案,需要:

  1. 使用锁,而不是同步块。
  2. 公平锁。
  3. 注意性能方面。

1.8.4 嵌套管程锁

类似死锁,但是是synchronized的嵌套造成的。
死锁与嵌套管程锁区别:

  1. 死锁中,二个线程都在等待对方释放锁。
  2. 嵌套管程锁死中,线程1持有锁A,同时等待从线程2发来的信号,线程2需要锁A来发信号给线程1。(采用加锁顺序也可能造成嵌套管程锁)

1.8.5 锁的可重入性

Java中的synchronized同步块是可重入的。这意味着如果一个java线程进入了代码中的synchronized同步块,并因此获得了该同步块使用的同步对象对应的管程上的锁,那么这个线程可以进入由同一个管程对象所同步的另一个java代码块。

1.9 锁的优化

1.9.1 自旋锁

互斥同步的时候,需要挂起线程和恢复线程等操作,并发性能会很低。可以通过等待一段时间,进行自身循环等待锁,这种技术就是自旋锁。
自旋锁不能代替阻塞,虽然自旋锁避免线程切换开销,但是本身对处理器资源消耗大。自适应的自旋锁会通过计算平均自旋次数决定下一次自旋。AtomicInteger等类就是采用自旋锁。

1.9.2 锁消除

Java1.6之后,编译时采用识别不存在锁的情况,从而自动消除锁,到达减轻加锁和解锁的开销。

1.9.3 锁粗化

当对一个共享数据的连续多次操作(每次都必须加锁和解锁),java可能会将这几个连续操作合并为一次操作并加锁。

1.9.4 轻量级锁

利用对象头存在的格外空间,标示对象的锁定状态。如下表格所示:

存储内容标志位状态
对象哈希码、对象分代年龄01未锁定
指向锁记录的指针00轻量级锁定
指向重量级锁的指针10膨胀(重量级锁定)
空,不需要记录信息11GC标记
偏向线程ID、偏向时间戳、对象分代年龄01可偏向

轻量级锁是使用标志位,在进入代码同步块时,如果同步对象没有被锁定,则现在线程栈中建立锁记录(Lock Record)的空间,拷贝对象到锁记录中,并使用CAS进行锁操作,将对象变成轻量级锁定(标志位为:00)。如果CAS操作失败,则膨胀升级,锁定标志位变成10。
轻量级锁在没有大量冲突的时候,效率会提升,但是如果大量冲突,那么他还需花费CAS的开销,所以性能反而下降。

1.9.5 偏向锁

如果说轻量级锁解决没有大量竞争情况,那么偏向锁就是解决无竞争需同步的情况。不需要进行CAS操作,但是一旦发现有竞争则升级锁。

1.10 Java中的锁

1.10.1 Volatile

由于此修饰词,变量的值在CPU寄存器或者CPU缓存中的值被写回到主存中。Volatile只是保证每次都是被写会内存,而且每次都从内存读最新的,但是volatitle并不能保证线程安全。一般用于只有一个线程写入,多个线程读取的时候使用。

1.10.2 Synchronized

同步块,在synchronized修饰下的方法或者代码块都具备原子性,保证线程安全。Synchronized的修饰如下:
第一种修饰方法:修饰普通方法
普通的同步方法。
public synchronized void methodAAA() {}
代表线程访问同一个实例时,互相排斥,但是如果访问不同对象则无关。上面例子等同于如下修饰:
使用同步块模式

public void methodAAA()  
{  
synchronized (this)      //  (1)  
{  
  //…..  
	}  
}  

第二种修饰方法:修饰静态方法

普通修饰静态方法

Class Foo  
{  
 	public synchronized static void methodAAA()   // 同步的static 函数  
}  

使用同步块修饰静态方法

//….  
	}  
public static  void methodBBB()  
{  
    synchronized(Foo.class)   //  class literal(类名称字面常量)  
}  
       } 

2段修饰的效果一样,所有对象对于静态方法的访问是相互排斥的。

Synchronized看似很完美,但其实它却存在缺点,比如它无法中断一个正在等候获得锁的线程,也无法通过投票得到锁,如果不想等下去,也就没法得到锁。同步还要求锁的释放只能在与获得锁所在的堆栈帧相同的堆栈帧中进行,多数情况下,这没问题(而且与异常处理交互得很好),但是,确实存在一些非块结构的锁定更合适的情况。

1.10.3 ReentrantLock

是java1.6之后加入的,修补synchronized的一些缺陷。包括可以采用不同调度算法,加了类似锁投票、定时锁等候和可中断锁等候的一些特性。使用方法如下:

显示全文