一、 线程和进程的概念以及简单实现
- 什么是进程
- 什么是线程
- 进程和线程的区别
- 简单实现线程
进程:一段程序的执行过程就叫做进程。也是在操作系统并发的一个任务。
系统中拥有资源的基本单位
。
线程:轻量级进程,程序中的一个顺序执行流程。是CPU的基本调度单位
。
进程和线程的区别:线程和进程的最大区别就是进程有自己一整套变量,而线程共享数据
。每个线程有独立的栈,共享堆空间数据,而进程拥有自己的堆空间。也就是说进程拥有资源,而线程并不拥有资源。现代os都将线程作为最小调度单位,进程作为资源分配的最小单位。 在windows中进程是不活动的,
只是作为线程的容器。
也就是说,java中的所有线程确实在JVM进程中,但是CPU调度的是进程中的线程。
简单实现线程:
- 继承Runnable接口实现run方法。
//继承Runnable接口
Runnable runnable = new Runnable(){
//覆盖run()方法
public void run(){ }
};
Thread t = new Thread(runnable); //把任务加入线程
t.start(); //开启线程
2.继承Thread类覆盖run方法。Thread类本身实现了Runnable接口,可以将其当作一个任务。
//创建一个类直接继承Thread类
class MyThread extends Thread{
//覆盖run()方法
public void run(){}
}
Thread t = new MyThread(); //把任务加入线程
t.start(); //开启线程
3.实现Callable接口,实现call方法,该方法有返回值。异步得到返回值。(jdk1.5配合连接池使用)
//创建线程池
ExecutorService es = Executors.newFixedThreadPool(x); //x为要准备的线程的数量
Callable callable = new Callable(){
public Object call(){}
};
es.submit(callable); //为线程池添加任务
es.shutdown(); //关闭线程池
二、线程的状态
- new 新创建
- Runnable 可运行
- Blocked 被阻塞
- waiting 等待
- Timed waiting 计时等待
- Terminated 被终止
- 守护线程
new 新创建状态:在堆空间开辟内存空间,与常规对象相同。
Runnable 可运行状态:调用start()
方法后,线程会进入可运行状态,但是并不是一定会运行,只有该线程被cpu选中获得cpu时间片才会进入运行状态,否则处于就绪状态。
Blocked 阻塞状态:线程因为某种原因,请求别的资源没有得到,该线程被挂起,这是就会处于阻塞状态。就算获得时间片也无法继续运行。(阻塞状态不会获得时间片)。
waiting 等待状态:无限期等待。当前线程中,如果对其他线程调用join()
方法,当前线程就只有在其他线程结束后才会执行调用join()
方法后的代码。直到其他线程结束,该线程才会被唤醒进入可运行(就绪)状态。等待状态是只要获得时间片就可以立即运行。而阻塞状态获得时间片也无法运行
。
Timed waiting 计时等待:调用sleep(x)
x为数字,单位为毫秒,该线程会进入等待状态,等待x秒。yield()
会使线程放弃cpu从可运行状态中的运行状态进入就绪状态(也是可运行状态)。
Terminated 终止:代码全部执行完毕,线程会结束。
守护线程:setDaemon(true)
设置线程为守护线程。当所有非守护线程都结束后,守护线程也会结束,之后进程结束。
三、线程安全
- 线程安全概念
- 同步代码块
- 同步方法和死锁
- Lock接口(jdk1.5)
- 活锁问题
- 读写锁
- 悲观锁和乐观锁
- Atomic包
- Fork — join 分治归并算法
线程安全概念:多个线程访问临界资源时,如果破坏代码的原子性操作,就会造成数据不一致。
同步代码块:对o加锁的同步代码块,只有拿到o对象的锁标记的线程才能进入代码块中。
synchronized(o){ 代码 }
同步方法和死锁:同步方法就是用synchornized
修饰的方法。只有拿到方法所在对象的锁标记才能进入到该方法中。
public synchronized void test(){ 代码 }
死锁是指多个线程因竞争资源而造成的一种互相阻塞
的状态。
synchronized(o1){
synchronized(o2){ 代码 }
}
synchronized(o2){
synchronized(o1){ 代码 }
}
Lock接口(jdk.1.5):Lock锁可以用来代替synchronized
关键字。使用Lock锁代码如下。
//获得一个锁对象
Lock lock = new ReentrantLock();
//加锁
lock.lock();
//解锁
lock.unlock();
//尝试得到锁标记,如果得不到返回false
lock.tryLock();
//获得一个等待队列对象,返回值是一个Condition队列对象
Conditon condition = lock.newCondition();
活锁问题:任务或者执行者没有被阻塞,但由于某些条件没有满足,导致一直重复尝试并且总是失败。
当使用tryLock()
时,如果获得锁标记失败之后释放锁标记,就可能因为双方线程都释放了对方的锁标记,然后又同时再次获得对方的锁标记从而进入活锁状态。
读写锁:读写锁是用读写分离的思想实现的锁。读锁可以重复分配
,而写锁只能分配一次
,也就是说读的时候可以很多人一起读,但是不能写入。写操作的时候只能一个线程写入,并且没有线程可以进行读操作。代码如下:
//获得读写锁
ReadWriteLock readWriteLock = new ReentrantReadWriteLock();
//获得读锁
Lock readLock = readWriteLock.readLock();
//获得写锁
Lock writeLock = readWriteLock.writeLock();
悲观锁和乐观锁:悲观锁是指对线程安全持悲观态度认为每次拿数据别人都会有人同时修改数据
,都会发生数据不一致问题。而乐观锁是认为每次拿数据都不会有人同时写入
,不会发生数据不一致问题。乐观锁适合于读操作比较多的场景
,可以提高代码吞吐量。
Atomic包:提供原子操作来进行基本数据类型的使用。
Fork — join 分治归并算法:专为多核cpu而生的算法。像使用此算法的ForkJoinPool线程池。就是把一个大任务拆分成许多小任务,最后汇总。
ForkJoinPool 线程池 默认线程数:CPU核数 工作窃取算法。
RecursiveTask 覆盖 compute() 有返回值, join():以同步的方式获得返回值。
RecursiveAction覆盖 compute() 无返回值。
四、线程通信
- 等待 — 通知机制
- 生产者 — 消费者问题
等待 — 通知机制:等待通知机制是指对o加锁的方法或同步代码块对o对象调用wait()
方法时,该线程会释放所持用的所有锁标记并进入等待状态(进入o的等待队列
)。 notify()/notifyAll()
必须出现对o加锁的同步方法或代码块中,当调用notify()
方法时,会随机唤醒等待队列中的一个线程进入阻塞状态等待获得锁标记后运行。而调用notifyAll()
方法时会唤醒等待队列中的所有方法进入阻塞状态。还可以用Lock接口下的await()
方法和signalAll()
方法替代,实现更精细的控制。
生产者 — 消费者模式:
wait()
/notifyAll()版本
:
public class WaitTest {
public static void main(String[] args) {
Stack s = new Stack();
Thread t1 = new Thread() {
//线程t1循环添加26个字母
public void run() {
for(char i = 'A';i<='Z';i++) {
s.push(i+"");
}
}
};
//线程t2循环删除26个字母
Thread t2 = new Thread() {
@Override
public void run() {
for(char i = 1;i <= 26;i++) {
s.pop();
}
}
};
//开启t1,t2线程
t1.start();
t2.start();
}
}
//创建一个栈
class Stack {
//封装一个有界数组
private String arr[] = new String[]{"","","","","","",""};
//数组内的元素个数,也可以理解为指针。
private int index;
//入栈方法
public synchronized void push(String s) {
//如果栈满了,就让该线程进入等待状态
while(index == arr.length) {
try {
//进入等待状态
this.wait();
} catch (InterruptedException e) {
e.printStackTrace();
}
}
arr[index] = s;
index++;
print();
System.out.println();
this.notify(); //释放锁标记,通知出栈线程可以进行出栈操作
}
//出栈方法
public synchronized void pop() {
//如果栈空了,就让该线程进入等待状态
while(index == 0) {
try {
//进入等待状态
this.wait();
} catch (InterruptedException e) {
// TODO Auto-generated catch block
e.printStackTrace();
}
}
index--;
String s = arr[index];
arr[index] = "";
print();
System.out.println();
this.notifyAll(); //释放锁标记,通知入栈线程可以进行入栈操作
}
//打印方法
public void print() {
for(String e:arr) {
System.out.print(e);
}
}
}
await()
/signalAll()版本
:
public class LockTest {
public static void main(String[] args) {
Stack s = new Stack();
Thread t1 = new Thread() {
//线程t1循环添加26个字母
public void run() {
for(char i = 'A';i<='Z';i++) {
s.push(i+"");
}
}
};
//线程t2循环删除26个字母
Thread t2 = new Thread() {
@Override
public void run() {
for(char i = 1;i <= 26;i++) {
s.pop();
}
}
};
//开启t1,t2线程
t1.start();
t2.start();
}
}
//创建一个栈
class Stack {
//封装一个有界数组
private String arr[] = new String[]{"","","","","","",""};
//数组内的元素个数,也可以理解为指针。
private int index;
//获得一个锁对象
Lock lock = new ReentrantLock();
//获得等待队列
Condition full = lock.newCondition(); //当栈满了要进入的队列
Condition empty = lock.newCondition(); //当栈空了要进入的队列
//入栈方法
public void push(String s) {
//如果栈空了,就让该线程进入等待状态
try {
//加锁
lock.lock();
while(index == arr.length) {
try {
//进入栈满了的等待队列
full.await();
} catch (InterruptedException e) {
e.printStackTrace();
}
}
arr[index] = s;
index++;
print();
System.out.println();
//通知消费者可以消费了
empty.signalAll();
}finally {
//如果出异常可能会直接返回而不释放锁标记,因此要把释放锁标记代码放入finally
lock.unlock();
}
}
//出栈方法
public synchronized void pop() {
//如果栈空了,就让该线程进入等待状态
try {
//加锁
lock.lock();
while(index == 0) {
try {
//让线程进入消费者等待队列
empty.await();
} catch (InterruptedException e) {
// TODO Auto-generated catch block
e.printStackTrace();
}
}
index--;
String s = arr[index];
arr[index] = "";
print();
System.out.println();
//通知生产者可以开始生产了
full.signalAll();
}finally {
//如果出异常可能会直接返回而不释放锁标记,因此要把释放锁标记代码放入finally
lock.unlock();
}
}
//打印方法
public void print() {
for(String e:arr) {
System.out.print(e);
}
}
}
线程池(jdk1.5)
- 使用线程池原因
- 线程池实现
使用线程池原因:创建线程和销毁线程是一个很浪费资源的操作,所以最好提前创建好一定数量的线程预备着,以便于使用。
线程池实现:
ExecutorService = Executors.newFixedThreadPool(x);//x为预先创建的线程的数量
线程安全的集合
- Collections集合工具类
- CopyOnWriteArrayList集合
- ConcurrentHashMap集合
- ConcurrentLinkedQueue集合
- BlockingQueue阻塞队列
Collections集合工具类:Collections.synchronizedXXX(集合)
方法可以讲线程不安全的集合通过给所有方法加上synchronized
关键字的方法将其变为一个线程安全的集合。
CopyOnWriteArrayList集合:如果同时有人进行读写操作,该集合会先返回原集合。然后将原集合拷贝到一个新集合中,在新集合中进行写操作,最后再覆盖掉原集合。该集合是利用复制数组的方式实现数组元素的修改
。写效率低 、读效率高 。适合读操作远多于写操作的场景
。
ConcurrentHashMap集合 :分段锁实现。具体是将该集合分为十六段,给每一段加锁。如果读写的不是同一段加锁的部分就不会阻塞。
ConcurrentLinkedQueue集合:线程安全的队列(链表实现
) 利用无锁算法实现线程安全 CAS。利用CAS无锁算法
实现,用的是一种乐观锁
实现线程安全的方式 (也就是不加锁)
。
BlockingQueue阻塞队列接口:put()
添加元素到队列如果队列满则等待,take()
删除队头元素如果队列空则等待。数组实现类实现的是有界队列。链表实现类是实现无界队列。无界队列队列不会满但是会空。
高级并发类
- Semaphore 信号量
- CountDownLatch 倒数器
- CyclicBarrier 循环倒数器
Semaphore 信号量:Semaphore相当于一把共享锁。创建对象时给一个参数代表有多少个许可证,只有拥有许可证才能进入。
Semaphore semaphore = new Semaphore(3); //相当于有三个许可证
semaphore.acquire(); //有空闲许可证就获得
semaphore.release(); //释放掉许可证
CountDownLatch 倒数器:创建对象时给定一个参数作为倒数器。先在x线程调用await()
方法该线程就会阻塞。然后其他线程每次调用countDown()
方法就会让倒数的数减一,当倒数的数为0时。x线程就会解除阻塞状态进入可运行状态。
CountDownLatch countDownLatch = new CountDownLatch(3); //倒数的数为3
countDownLatch.await(); //调用该方法线程会阻塞
countDownLatch.countDown(); //调用该方法倒数器会减一
CyclicBarrier 循环倒数器:给一个参数作为倒数器。当线程没调用一次await()方法线程就会阻塞,同时倒数器减一。当倒数器为0时所有线程同时释放进入可运行状态。同时倒数器重置。
CyclicBarrier cyclicBarrier = new CyclicBarrier(3); //设置倒数器为3
cyclicBarrier.await(); //调用后该线程会阻塞并使倒数器减一