1. 什么是线程
线程(thread)是指一个任务从头至尾的执行流,线程提供一个运行任务的机制,对于java而言,一个程序中可以并发的执行多个线程,这些线程可以在多处理器系统上同时运行。当程序作为一个应用程序运行时,java解释器为main()方法启动一个线程。
2. 线程和进程
区别和联系:
1. 进程中可以包含多个线程,线程必须存在于某个进程实体中 2. 进程在进行上下文切换时由于要切换页表,往往伴随者页调 度,因此开销比较大,而线程在进行上下文切换时,由于仅 涉及与自身相关的寄存器状态和栈的信息(线程的上下文环 境主要包含寄存器的值、程序计数器、栈指针),因此开销 比较小; 3. 系统在运行时会为每个进程分配不同的内存区域,但是不会 为线程分配内存,同一个进程中的各个线程共享该进程的内 存区域。 4. 与进程的控制表PCB相似,线程也有自己的控制表TCB,但是 TCB中所保存的线程状态比PCB表中少多了。 5. 独立进程间的通信要与核心交互,而由于同一进程中的线程 共享内存,它们之间的通信就不需要调用核心。
3. 线程创建
- 需要从Java.lang.Thread类派生一个新的线程类,重载它的run()方法;
- 实现Runnalbe接口,重载Runnalbe接口中的run()方法。
两者区别:
使用实现Runnable接口方式创建线程可以共享同一个目标对象(TreadDemo1 tt=new TreadDemo1();),实现了多个相同线程处理同一份资源.而继承Thread创建线程的方式,new出了两个任务类对象,有各自的成员变量,相互之间不干扰。
一、采用继承Thread类方式:
(1)优点:编写简单,如果需要访问当前线程,无需使用Thread.currentThread()方法,直接使用this,即可获得当前线程。
(2)缺点:因为线程类已经继承了Thread类,所以不能再继承其他的父类。
二、采用实现Runnable接口方式:
(1)优点:线程类只是实现了Runable接口,还可以继承其他的类。在这种方式下,可以多个线程共享同一个目标对象,所以非常适合多个相同线程来处理同一份资源的情况,从而可以将CPU代码和数据分开,形成清晰的模型,较好地体现了面向对象的思想。
(2)缺点:编程稍微复杂,如果需要访问当前线程,必须使用Thread.currentThread()方法。
4. 线程的生命周期
(1)生命周期的五种状态
新建(new Thread)
当创建Thread类的一个实例(对象)时,此线程进入新建状态(未被启动)。
例如:Thread t1=new Thread();
就绪(runnable)
线程已经被启动,正在等待被分配给CPU时间片,也就是说此时线程正在就绪队列中排队等候得到CPU资源。例如:t1.start();
运行(running)
线程获得CPU资源正在执行任务(run()方法),此时除非此线程自动放弃CPU资源或者有优先级更高的线程进入,线程将一直运行到结束。
死亡(dead)
当线程执行完毕或被其它线程杀死,线程就进入死亡状态,这时线程不可能再进入就绪状态等待执行。
自然终止:正常运行run()方法后终止
异常终止:调用stop()方法让一个线程终止运行
堵塞(blocked)
由于某种原因导致正在运行的线程让出CPU并暂停自己的执行,即进入堵塞状态。
正在睡眠:用sleep(long t) 方法可使线程进入睡眠方式。一个睡眠着的线程在指定的时间过去可进入就绪状态。
正在等待:调用wait()方法。(调用notify()方法回到就绪状态)
被另一个线程所阻塞:调用suspend()方法。(调用resume()方法恢复)
5. 线程安全
1). 什么是线程安全?线程安全是怎么完成的(原理)?
线程安全就是说多线程访问同一代码,不会产生不确定的结果。编写线程安全的代码是低依靠线程同步。
2). 线程安全的类:
StringBuffer:线程安全的可变字符序列。一个类似于 String的字符串缓冲区,但不能修改。
Vector:Vector类可以实现可增长的对象数组。
Hashtable:此类实现一个哈希表,该哈希表将键映射到相应的值。任何非 null对象都可以用作键或值。
API 搜索Collections,查看方法
A:public static <T> Collection<T> synchronizedCollection(Collection<T> c):返回指定 collection 支持的同步(线程安全的)collection。
B:public static <T> List<T> synchronizedList(List<T> list):返回指定列表支持的同步(线程安全的)列表。
C:public static <K,V> Map<K,V> synchronizedMap(Map<K,V> m):返回由指定映射支持的同步(线程安全的)映射。
D:public static <T> Set<T> synchronizedSet(Set<T> s):返回指定 set 支持的同步(线程安全的)set。
E:public static <K,V> SortedMap<K,V> synchronizedSortedMap(SortedMap<K,V> m):返回指定有序映射支持的同步(线程安全的)有序映射。
F:public static <T> SortedSet<T> synchronizedSortedSet(SortedSet<T> s):返回指定有序 set 支持的同步(线程安全的)有序 set。
6.线程锁
1. synchronized
把代码块声明为 synchronized,有两个重要后果,通常是指该代码具有 原子性(atomicity)和 可见性(visibility)。
1 原子性
原子性意味着个时刻,只有一个线程能够执行一段代码,这段代码通过一个monitor object保护。从而防止多个线程在更新共享状态时相互冲突。
2 可见性
可见性则更为微妙,它要对付内存缓存和编译器优化的各种反常行为。它必须确保释放锁之前对共享数据做出的更改对于随后获得该锁的另一个线程是可见的 。
作用:如果没有同步机制提供的这种可见性保证,线程看到的共享变量可能是修改前的值或不一致的值,这将引发许多严重问题。
原理:当对象获取锁时,它首先使自己的高速缓存无效,这样就可以保证直接从主内存中装入变量。 同样,在对象释放锁之前,它会刷新其高速缓存,强制使已做的任何更改都出现在主内存中。 这样,会保证在同一个锁上同步的两个线程看到在 synchronized 块内修改的变量的相同值。
一般来说,线程以某种不必让其他线程立即可以看到的方式(不管这些线程在寄存器中、在处理器特定的缓存中,还是通过指令重排或者其他编译器优化),不受缓存变量值的约束,但是如果开发人员使用了同步,那么运行库将确保某一线程对变量所做的更新先于对现有synchronized块所进行的更新,当进入由同一监控器(lock)保护的另一个synchronized块时,将立刻可以看到这些对变量所做的更新。类似的规则也存在于volatile变量上。
——volatile只保证可见性,不保证原子性!
synchronize的限制
synchronized是不错,但它并不完美。它有一些功能性的限制:
- 它无法中断一个正在等候获得锁的线程;
- 也无法通过投票得到锁,如果不想等下去,也就没法得到锁;
- 同步还要求锁的释放只能在与获得锁所在的堆栈帧相同的堆栈帧中进行,多数情况下,这没问题(而且与异常处理交互得很好),但是,确实存在一些非块结构的锁定更合适的情况。
2.ReentrantLock
Java.util.concurrent.lock 中的Lock框架是锁定的一个抽象,它允许把锁定的实现作为 Java 类,而不是作为语言的特性来实现。这就为Lock的多种实现留下了空间,各种实现可能有不同的调度算法、性能特性或者锁定语义。
ReentrantLock类实现了Lock,它拥有与synchronized 相同的并发性和内存语义,但是添加了类似锁投票、定时锁等候和可中断锁等候的一些特性。此外,它还提供了在激烈争用情况下更佳的性能。(换句话说,当许多线程都想访问共享资源时,JVM 可以花更少的时候来调度线程,把更多时间用在执行线程上。
class Outputter1 {
private Lock lock = new ReentrantLock();// 锁对象
public void output(String name) {
lock.lock(); // 得到锁
try {
for(int i = 0; i < name.length(); i++) {
System.out.print(name.charAt(i));
}
} finally {
lock.unlock();// 释放锁
}
}
}
区别:
需要注意的是,用sychronized修饰的方法或者语句块在代码执行完之后锁自动释放,而是用Lock需要我们手动释放锁,所以为了保证锁最终被释放(发生异常情况),要把互斥区放在try内,释放锁放在finally内!!
3.读写锁ReadWriteLock
上例中展示的是和synchronized相同的功能,那Lock的优势在哪里?
例如一个类对其内部共享数据data提供了get()和set()方法,如果用synchronized,则代码如下:
class syncData {
private int data;// 共享数据
public synchronized void set(int data) {
System.out.println(Thread.currentThread().getName() + "准备写入数据");
try {
Thread.sleep(20);
} catch (InterruptedException e) {
e.printStackTrace();
}
this.data = data;
System.out.println(Thread.currentThread().getName() + "写入" + this.data);
}
public synchronized void get() {
System.out.println(Thread.currentThread().getName() + "准备读取数据");
try {
Thread.sleep(20);
} catch (InterruptedException e) {
e.printStackTrace();
}
System.out.println(Thread.currentThread().getName() + "读取" + this.data);
}
}
然后写个测试类来用多个线程分别读写这个共享数据:
public static void main(String[] args) {
// final Data data = new Data();
final syncData data = new syncData();
// final RwLockData data = new RwLockData();
//写入
for (int i = 0; i < 3; i++) {
Thread t = new Thread(new Runnable() {
@Override
public void run() {
for (int j = 0; j < 5; j++) {
data.set(new Random().nextInt(30));
}
}
});
t.setName("Thread-W" + i);
t.start();
}
//读取
for (int i = 0; i < 3; i++) {
Thread t = new Thread(new Runnable() {
@Override
public void run() {
for (int j = 0; j < 5; j++) {
data.get();
}
}
});
t.setName("Thread-R" + i);
t.start();
}
}
运行结果:
Thread-W0准备写入数据
Thread-W0写入0
Thread-W0准备写入数据
Thread-W0写入1
Thread-R1准备读取数据
Thread-R1读取1
Thread-R1准备读取数据
Thread-R1读取1
Thread-R1准备读取数据
Thread-R1读取1
Thread-R1准备读取数据
Thread-R1读取1
Thread-R1准备读取数据
Thread-R1读取1
Thread-R2准备读取数据
Thread-R2读取1
Thread-R2准备读取数据
Thread-R2读取1
Thread-R2准备读取数据
Thread-R2读取1
Thread-R2准备读取数据
Thread-R2读取1
Thread-R2准备读取数据
Thread-R2读取1
Thread-R0准备读取数据 //R0和R2可以同时读取,不应该互斥!
Thread-R0读取1
Thread-R0准备读取数据
Thread-R0读取1
Thread-R0准备读取数据
Thread-R0读取1
Thread-R0准备读取数据
Thread-R0读取1
Thread-R0准备读取数据
Thread-R0读取1
Thread-W1准备写入数据
Thread-W1写入18
Thread-W1准备写入数据
Thread-W1写入16
Thread-W1准备写入数据
Thread-W1写入19
Thread-W1准备写入数据
Thread-W1写入21
Thread-W1准备写入数据
Thread-W1写入4
Thread-W2准备写入数据
Thread-W2写入10
Thread-W2准备写入数据
Thread-W2写入4
Thread-W2准备写入数据
Thread-W2写入1
Thread-W2准备写入数据
Thread-W2写入14
Thread-W2准备写入数据
Thread-W2写入2
Thread-W0准备写入数据
Thread-W0写入4
Thread-W0准备写入数据
Thread-W0写入20
Thread-W0准备写入数据
Thread-W0写入29
现在一切都看起来很好!各个线程互不干扰!等等。。读取线程和写入线程互不干扰是正常的,但是两个读取线程是否需要互不干扰??
对!读取线程不应该互斥!
我们可以用读写锁ReadWriteLock实现:
import java.util.concurrent.locks.ReadWriteLock;
import java.util.concurrent.locks.ReentrantReadWriteLock;
class Data {
private int data;// 共享数据
private ReadWriteLock rwl = new ReentrantReadWriteLock();
public void set(int data) {
rwl.writeLock().lock();// 取到写锁
try {
System.out.println(Thread.currentThread().getName() + "准备写入数据");
try {
Thread.sleep(20);
} catch (InterruptedException e) {
e.printStackTrace();
}
this.data = data;
System.out.println(Thread.currentThread().getName() + "写入" + this.data);
} finally {
rwl.writeLock().unlock();// 释放写锁
}
}
public void get() {
rwl.readLock().lock();// 取到读锁
try {
System.out.println(Thread.currentThread().getName() + "准备读取数据");
try {
Thread.sleep(20);
} catch (InterruptedException e) {
e.printStackTrace();
}
System.out.println(Thread.currentThread().getName() + "读取" + this.data);
} finally {
rwl.readLock().unlock();// 释放读锁
}
}
}
测试结果:
Thread-W1准备写入数据
Thread-W1写入9
Thread-W1准备写入数据
Thread-W1写入24
Thread-W1准备写入数据
Thread-W1写入12
Thread-W0准备写入数据
Thread-W0写入22
Thread-W0准备写入数据
Thread-W0写入15
Thread-W0准备写入数据
Thread-W0写入6
Thread-W0准备写入数据
Thread-W0写入13
Thread-W0准备写入数据
Thread-W0写入0
Thread-W2准备写入数据
Thread-W2写入23
Thread-W2准备写入数据
Thread-W2写入24
Thread-W2准备写入数据
Thread-W2写入24
Thread-W2准备写入数据
Thread-W2写入17
Thread-W2准备写入数据
Thread-W2写入11
Thread-R2准备读取数据
Thread-R1准备读取数据
Thread-R0准备读取数据
Thread-R0读取11
Thread-R1读取11
Thread-R2读取11
Thread-W1准备写入数据
Thread-W1写入18
Thread-W1准备写入数据
Thread-W1写入1
Thread-R0准备读取数据
Thread-R2准备读取数据
Thread-R1准备读取数据
Thread-R2读取1
Thread-R2准备读取数据
Thread-R1读取1
Thread-R0读取1
Thread-R1准备读取数据
Thread-R0准备读取数据
Thread-R0读取1
Thread-R2读取1
Thread-R2准备读取数据
Thread-R1读取1
Thread-R0准备读取数据
Thread-R1准备读取数据
Thread-R0读取1
Thread-R2读取1
Thread-R1读取1
Thread-R0准备读取数据
Thread-R1准备读取数据
Thread-R2准备读取数据
Thread-R1读取1
Thread-R2读取1
Thread-R0读取1
与互斥锁定相比,读-写锁定允许对共享数据进行更高级别的并发访问。虽然一次只有一个线程(writer 线程)可以修改共享数据,但在许多情况下,任何数量的线程可以同时读取共享数据(reader 线程)
从理论上讲,与互斥锁定相比,使用读-写锁定所允许的并发性增强将带来更大的性能提高。
在实践中,只有在多处理器上并且只在访问模式适用于共享数据时,才能完全实现并发性增强。——例如,某个最初用数据填充并且之后不经常对其进行修改的 collection,因为经常对其进行搜索(比如搜索某种目录),所以这样的 collection 是使用读-写锁定的理想候选者。
4.volatile
用volatile修饰的变量,线程在每次使用变量的时候,都会读取变量修改后的最的值。volatile很容易被误用,用来进行原子性操作。
5. 读写锁和互斥锁
读写锁特点:
1)多个读者可以同时进行读
2)写者必须互斥(只允许一个写者写,也不能读者写者同时进行)
3)写者优先于读者(一旦有写者,则后续读者必须等待,唤醒时优先考虑写者)
互斥锁特点:
一次只能一个线程拥有互斥锁,其他线程只有等待