1.什么是进程
进程: 是OS中可以并发执行的一个任务.
运行方式:采用分时间片轮转方式,由OS进行调度。 【微观串行 宏观并行】
在一个操作系统中可以同时运行多个进程,这就是进程的并发
2.什么是线程
【由于 Java 代码是运行在 JVM 中的,对于某个操作系统来说,一个 JVM 就相当于一个进程,而 Java 代码不能够越过 JVM 直接与操作系统打交道,因此,Java 语言中没有多进程的概念,Java 中的并发,采用的是线程的概念】
线程: 进程中并发执行的一个顺序流程.
线程的组成
① CPU时间片 OS调度
② 内存
JVM内存 栈空间独立 堆空间共享
堆空间:保存对象(实例变量)
栈空间:保存局部变量
③ 代码
由程序员决定
切换进程时,堆空间和栈空间都切换,切换线程时,只切换栈空间,所以说,线程是轻量级的进程!
3.Thread类
Thread类:该类型的对象代表一个线程.
线程启动之后,会执行 线程对象.run();
Thread类中run方法定义什么代码,线程启动后默认执行相应的流程
4.线程的2种创建方法
public class ThreadTest {
//main线程负责执行main方法中所有的代码
public static void main(String[] args) {
//创建线程对象,并没有启动线程
Thread t1 = new MyThread();
Runnable target = new MyTarget();
Thread t2 = new Thread(target);
//创建完线程对象之后,只有调用 start(),系统中才真正启动了一个新线程
t1.start();
t2.start();
}
}
//1.继承Thread,重写run方法
class MyThread extends Thread{
//3无方法:无返回值 无形参 无可抛出的异常
public void run(){
for(int i = 1; i <= 100; i++){
System.out.println("$$$"+i);
}
}
}
//2.实现Runnable接口 提供run方法实现
class MyTarget implements Runnable{
public void run(){
for(int i = 1; i<=100; i++){
System.out.println("###"+i);
}
}
}
5.Runnable接口(三无接口)的缺陷:
没有返回值
不能抛异常
没有形参
interface Callable
public T call() throws Exception;
Future f = 线程池.submit(Callabel c);
T result = f.get();
线程会把返回值存入Future对象,调用get方法可能能够拿到返回值, 也可能阻塞,直到拿到返回值
6.线程池 since JDK5
ExecutorService es = Executors.newFixedThreadPool(2);//创建了固定长度为2的线程池
Runnable task1 = new Task1();
Runnable task2 = new Task2();
es.submit(task1);
es.submit(task2);
es.shutsown();
好处:便于线程资源的重复利用
7.fork-join框架(分治-归并算法) since JDK7
大任务 -----> 若干小任务 -----> 将小任务的运算结果合并成完整的结果
工作窃取算法: 当一个CPU内核完成了任务队列时,会从其他内核的任务队列末尾"偷"一个任务来完成
8.join()
除了使用 sleep()和等待 IO 之外,还有一个方法会导致线程阻塞,这就是线程的 join()方法。
public class JoinTest {
public static void main(String[] args) {
final Thread t1 = new Thread(){
public void run(){
for(int i = 0; i < 100; i++){
System.out.println("$$$"+i);
}
}
};
Thread t2 = new Thread(){
public void run(){
for(int i = 0; i < 100; i++){
//当t2运行到50时,调用t1.join(),会把自己阻塞,等t1运行结束,t2再接着运行
if(i == 50){
try {
t1.join();
} catch (InterruptedException e) {
e.printStackTrace();
}
}
System.out.println("###"+i);
}
}
};
t1.start();
t2.start();
}
}
9.线程同步
线程同步:多线程环境下,并发访问同1个对象(临界资源),由于彼此间切割对方的原子性操作,
对象的状态就会处于不一致的状态.
Java中任意对象都有一把互斥锁标记用于分配给线程
synchronized 关键字:同步
1. 同步代码块
synchronized(锁对象){
}
当一个线程要执行同步代码块时,必须获取锁对象的互斥锁标记,没有获取到,那么线程阻塞.
直到得到互斥锁标记,线程执行同步代码块,执行完毕后自动释放互斥锁标记,归还给锁对象.
线程是否同步,取决于线程是否争用同1个锁对象.
Object o1 = new Object();
Object o2 = new Object();
Object o3 = o1;
t1->synchronized(o1){
}
t2->synchronized(o2){
}
t3->synchronized(o1){
}
t4->synchronized(o3){
}
t1和t2不同步,t1和t3,t4同步
2.同步方法
synchronized作为修饰符修饰方法
当其修饰实例方法时,线程要执行同步实例方法,必须获取当前对象的互斥锁标记,不能获取就阻塞,
获取互斥锁标记,执行完毕同步方法,自动释放互斥锁标记给当前对象.
一个线程可以在持有互斥锁标记的前提下获取其他锁对象的互斥锁标记.
10.线程间通信 等待-通知机制
t1: o.wait() 必须出现在对o加锁的同步代码块中,t1会释放他拥有的所有锁标记,进入o的等待队列(阻塞)
synchronized(o){
o.wait();
}
t2: o.notify()/notifyAll() 也要放在对o加锁的同步代码块,t2会从o的等待队列中释放一个/全部线程
synchronized(o){
o.notifyAll();
}
生产者消费者问题
class Stacks{
private String[] data = new String[6];
private int index = 0;//数组下标
public synchronized void push(String s){//生产者
while (data.length == index) {//栈满的时候,阻塞自己
try {
this.wait();
} catch (InterruptedException e) {
e.printStackTrace();
}
}
data[index] = s;
System.out.println(s + " 压入栈中!");
index++;
print();
//最后要释放等待队列的线程
this.notifyAll();
}
public synchronized void pop(){//消费者
while (index == 0) {//栈空的时候,阻塞自己
try {
this.wait();
} catch (InterruptedException e) {
e.printStackTrace();
}
}
index--;
System.out.println(data[index] + " 出栈");
data[index] = "";
print();
//最后要释放等待队列的线程
this.notifyAll();
}
public void print(){
for (int i = 0; i < index; i++) {
System.out.print(data[i]+"\t");
}
System.out.println();
}
}
11.面试题:wait()和sleep()方法区别
wait()和sleep()方法区别: wait会失去锁标记,sleep不会失去锁标记
Lock:锁接口 提供了比synchronized更广泛的锁定语义
ReadWriteLock:读写锁 持有一对读锁和写锁
读锁:允许分配给多个线程
写锁:最多只允许分配给一个线程
在线程持有读锁时,不能分配写锁给其它线程.同样,在线程持有写锁时,也不能分配读锁给其它线程.
读读并发 写写互斥 读写互斥
List
- ArrayList
- Vector
- CopyOnWriteArrayList 写时复制 适用于读远多于写的场景
Set
- HashSet
- TreeSet
- CopyOnWriteArraySet
Map
- HashMap
- Hashtable
- ConcurrentHashMap jdk1.7 之前 采用分段锁 默认分为16段 允许任意线程的读,一定数量线程的写
jdk1.8之后 抛弃分段锁 改用CAS算法
· CAS算法:(Compare And Swap)
① 获取对象的状态s1
② 计算出对象的预期结果s2
③ 再次获取对象状态s3并和s1比较,状态相同则使用s2更新对象,状态不同,则选择失败.
失败后,可以再次重复上述3步操作
ExecutorService:线程池接口
sumbit(Runnable r) 向线程池中提交任务
Executors
ExecutorService es = Executors.newFixedThreadPool(int n)// 返回固定数量线程的线程池
ExecutorService es = Executors.newCachedThreadPool();//返回线程数量可变的线程池
线程池的作用:
① 可以重用线程 提高程序的性能
② 可以控制程序的线程数量,避免由于大量线程对象的创建,系统资源耗尽而引发宕机.
Callable: 有返回结果的任务接口
class Task implements Callable{
public T call(){
return null;
}
}
Future: 表示异步任务的结果
Future f = es.submit(new Task());
f.get()//获取结果
见我另一篇博客。