多线程的概念及一些常用类和方法
1.概念
进程: 操作系统(OS)中并发的一个任务
CPU: 分时间片 宏观并行 微观串行 由操作系统(OS)负责调度
线程: 在一个进程中,并发的一个顺序执行流程
2.线程的组成:
(1)CPU
特点:由OS负责分配
(2)Data
特点 : 堆空间共享 栈空间独立
堆空间: 存储对象 (存储实例变量)
栈空间: 存储局部变量
(3)代码
由程序员指定 :继承Thread类(覆盖run())或者实现Runnable接口中的run()
3.状态关系图
4.join和sleep方法
一个Thread对象代表一个线程 当线程t1对线程t2调用join() 则t1阻塞 当t2进入终止状态时,t1回到可运行状态
package day17hot1;
public class TestThread2 {
public static void main(String[] args) {
Thread t1 = new ThreadA();
Runnable t2 = new ThreadB(t1);
Thread t3 = new Thread(t2);
t1.start();
t3.start();
}
}
class ThreadA extends Thread{
public void run() {
for(int i = 1;i <= 99;i+=2) {
System.out.println("***" + i);
try {
Thread.sleep(100);
} catch (InterruptedException e) {
e.printStackTrace();
}
}
}
}
class ThreadB implements Runnable{
Thread t;
public ThreadB(Thread t) {
this.t = t;
}
public void run() {
for(int i = 2;i <= 100;i+=2) {
System.out.println("###" + i);
try {
synchronized (t) {
t.join();
}
} catch (InterruptedException e1) {
e1.printStackTrace();
}
try {
Thread.sleep(100);
} catch (InterruptedException e) {
e.printStackTrace();
}
}
}
}
当多线程并发访问临界资源(同一个对象)时,如果破坏了原子操作(不可分割的操作),就会造成数据不一致,这种现象就叫做线程的同步. 例如:用多线程控制栈的push和pop
5.线程池
线程池
①线程池: 线程容器,将预先创建的线程对象存到线程池中,只要将任务提交给线程池,
会分配线程对象完成提交任务,线程池中的线程对象可以被重复使用
②好处: 避免频繁的创建线程和销毁,从而提高空间利用率和执行效率
③线程池常用的接口和类:(位于java.util.concurrent包中)
a.Executor:线程池的顶级接口
b.ExecutorService:是Executor的子接口,线程池的核心接口
(1)submit(Runnable task):将线程任务提交给线程池
(2)shutdown():关闭线程池,将线程池的线程对象全部销毁
c.Executors: 获取线程池对象的工具类,其中方法基本都为静态方法
static ExecutorService newFixedThreadPool(int n):获取一个固定数量线程的线程池
参数指定线程池中线程对象的数量
static ExecutorService newCachedThreadPool():获取动态数量线程对象的线程池,
根据提交的任务需求,不够用时,则自动完成线程创建
Callable
a.Callable接口: 位于java.util.concurrent包中,类似于Runnable接口的应用,对应的对象代表线程任务。
注意: Callable是泛型接口,泛型约束了接口中方法的返回值数据类型
b.接口中的方法: V call(): 带有返回值的方法,同时可以抛出异常
Future f = pool.submit(c1);
c.Future是存储submit提交任务执行之后的返回值
利用Future中的get方法获取执行的结果
6.synchronized
概念及特点:
(1) 任何对象都有一个互斥锁标记,用来分配给线程
(2) 只有拿到对象锁标记的线程,才能进入对该对象加锁的同步代码块
(3) 线程退出同步代码块,会释放相应的锁标记
同步方法:在整个方法的范围内,对当前对象加锁
只有拿到对象锁标记的线程,才能调用该对象的同步方法
锁池:任何对象都有的一个空间,用来存放等待该对象锁标记的线程
注意:一个线程可以同时拥有多个对象的锁标记,当线程阻塞在锁池中时,不会释放其已经拥有的锁标记,由此可能造成死锁
7. Lock
Lock
a.Lock:接口,位于java.util.concurrent.locks包中,代表锁
b.Lock中常用方法:
(1)void lock(): 获取锁
(2)void unlock(): 释放锁
c.实现类: ReentrantLock
8.解决死锁的办法
建立线程间的通信:等待(wait)-通知(notify)
任何对象都有一个等待队列,用来存放线程
线程t1 对o调用wait() 必须放在对o加锁的同步代码块中
效果: (1) t1会释放其拥有的所有锁标记; (2) 同时t1会阻塞在o的等待队列中
线程t2 对o调用notify()/notifyAll()必须放在对o加锁的同步代码块
效果: 从o的等待队列中释放一个/全部线程
package day17hot1;
public class TestWaitNotify {
public static void main(String[] args) throws Exception {
Object o = new Object();
Thread t = new SubThread(o);
t.start();
synchronized (o) {
System.out.println("main1 ...");
o.wait();
System.out.println("main2 ...");
}
}
}
class SubThread extends Thread {
Object o;
public SubThread(Object o) {
this.o = o;
}
@Override
public void run() {
synchronized (o) {
System.out.println("Sub1 ...");
o.notifyAll();
System.out.println("Sub2 ...");
}
}
}
9.集合的扩充(CopyOnWriteArrayList)
1.CopyOnWriteArrayList
(1)位于:java.util.concurrent包中
(2)在所有读操作(不会改变集合中数据内容的操作)中不加锁;在写操作时加锁,为了保证数据正确性,
写操作时,拷贝一个副文本,在副文本上进行写操作,写操作完成之后,新数据替换旧数据允许多个
线程同时进行读操作,但是同一时间只允许一个线程进行写操作,同时一个线程进行写操作,也允许
多个线程进行读操作。(牺牲写操作的效率提高读操作的效率)
(3)应用场景: 读操作的次数远远大于写操作次数时
(4)CopyOnWriteArrayList和Vector的共同特点和区别
区别:CopyOnWriteArrayList在读次数远远多于写操作时,效率远高于Vector,仅次于ArrayList
共同点:都是线程安全
CopyOnWriteArrayList和ArrayList
区别:CopyOnWriteArrayList线程安全,ArrayList线程不安全
共同点:用法和ArrayList一样(遍历方式等)
10.面试题: ArrayList和Vector的区别
ArrayList: 所有方法都不是同步方法 可能会造成数据不一致 线程不安全 多线程并发时效率高
Vector: 方法基本上都是同步方法 线程安全 多线程并发时效率低
11.synchronized和其他修饰符的组合
除了abstract不能和synchronized组合,其他的都可以搭配
static和synchronized组合: 表示给当前类对象加锁
注意: synchronized不能修饰构造方法!