多线程
1.进程
1.1 运行时的程序,称为进程。
2. 线程
称为轻量级进程,程序中的一个顺序控制流程,同时也是CPU的基本调度单位,进程由多个线程组成,彼此之间完成不同的工作,交替执行
2.2. 线程的组成
CPU时间片:操作系统(OS)会为每个线程分配执行时间2.2.2. 运行数据· 堆空间:存储线程需使用的对象,多个线程可以共享堆中的对象· 栈空间:存储线程需使用的局部变量,每个线程都拥有独立的栈
2.3. 线程的创建
2.3.1. 创建线程的第一种方式:·
class MyThread extends Thread{
//逻辑代码
}
2.3.2. 创建线程的第二种方式:·
class MyThread implements Runnable{
//逻辑代码
}
2.4.线程安全问题
线程不安全· 当线程并发访问临界资源时,如果破坏原子操作,可能会造成数据不一致· 临界资源:共享资源(同一对象),一次仅允许一个线程使用。才可以保证其正确性· 原子操作:不可分割的多部操作,被视作一个整体,其顺序和步骤不可打乱或缺省
2.5.线程的生命周期
线程具有生命周期,其中包含了初识状态、就绪状态、运行状态、等待状态、休眠状态、阻塞状态和死亡状态。
初识状态(New):
当线程对象对创建后,即进入了新建状态,如:Thread t = new MyThread();
就绪状态(Runnable):
当调用线程对象的start()方法,线程即进入就绪状态。处于就绪状态的线程,只是说明此线程已经做好了准备,随时等待CPU调度执行,并不是说执行了t.start()此线程立即就会执行;
运行状态(Running):
当CPU开始调度处于就绪状态的线程时,此时线程才得以真正执行,即进入到运行状态。
注:就 绪状态是进入到运行状态的唯一入口,也就是说,线程要想进入运行状态执行,首先必须处于就绪状态中;
等待状态(wait()):
当线程调用Thread类中的wait()方法的时候,该线程就会进入等待状态。进入等待状态,就意味着会被唤醒,而唤醒该线程的方法就是调用Thread类中的notify()方法。
注:notifyAll()方法是将所有处于等待状态下的线程唤醒。
休眠状态(sleep()):
当线程调用Thread类中的sleep()方法的时候,则会进入休眠状态:
for(int i =1;i<=50;i++) {
System.out.println(Thread.currentThread().getName()+" - "+i);
if(i == 30) {//特定条件下休眠
Thread.sleep(2000);//有限期等待。等待时间由参数的毫秒值决定
}
}
阻塞状态(Blocked):
处于运行状态中的线程由于某种原因,暂时放弃对CPU的使用权,停止执行,此时进入阻塞状态,直到其进入到就绪状态,才 有机会再次被CPU调用以进入到运行状态。根据阻塞产生的原因不同,阻塞状态又可以分为三种:
(1)等待阻塞:运行状态中的线程执行wait()方法,使本线程进入到等待阻塞状态;
(2)同步阻塞 : 线程在获取synchronized同步锁失败(因为锁被其它线程所占用),它会进入同步阻塞状态;
(3)其他阻塞 :通过调用线程的sleep()或join()或发出了I/O请求时,线程会进入到阻塞状态。当sleep()状态超时、join()等待线程终止或者超时、或者I/O处理完毕时,线程重新转入就绪状态。
死亡状态(Dead):
线程执行完了或者因异常退出了run()方法,该线程结束生命周期。
阻塞案例:
package t3;
public class TestDeadLock {
public static void main(String[] args
LeftChopstick left = new LeftChopstick();
RightChopstick right = new RightChopstick();
Thread boy = new Thread(new Boy(left,right));
Thread girl = new Thread(new Girl(left,right));
boy.start();
girl.start();
}
}
class LeftChopstick{
String name = "左筷子";
}
class RightChopstick{
String name = "右筷子";
}
class Boy implements Runnable{
LeftChopstick left;
RightChopstick right;
public Boy(LeftChopstick left,RightChopstick right) {
this.left =left;
this.right = right;
}
public void run() {
System.out.println("男孩要拿筷子!");
synchronized(left) {
//拿到左筷子资源,加锁!
try {
left.wait();//把筷子资源让出去!女士优先!
} catch (InterruptedException e) { e.printStackTrace();
}
System.out.println("男孩拿到了左筷子,开始拿右筷子"); synchronized(right){//拿到右筷子资源,加锁!
System.out.println("男孩拿到了右筷子,开始吃饭");
}
}
}
}
class Girl implements Runnable{
LeftChopstick left;
RightChopstick right;
public Girl(LeftChopstick left,RightChopstick right) {
this.left =left;
this.right = right;
}
public void run() {
System.out.println("女孩要拿筷子!");
synchronized(right) {//拿到右筷子资源,加锁!
System.out.println("女孩拿到了右筷子,开始拿左筷子");
synchronized(left){//拿到左筷子资源,加锁!
System.out.println("女孩拿到了左筷子,开始吃饭");
left.notify();//女孩吃完后,唤醒等待左筷子锁的男孩线程
}
}
}
}
3.多线程中的常见方法
3.1. 休眠
public static void sleep(long millis)
当前线程主动休眠millis毫秒
3.2. 放弃
public static void yield()
当前线程主动放弃时间片,回到就绪状态,竞争下一次时间片。
3.3. 结合
public final void join()
允许其他线程加入到当前线程中
4.同步
4.1. 同步方式(1)
4.1.1. 同步代码块
synchronized(临界资源对象){
//对临界资源对象加锁
//代码(原子操作)
}
4.1.2. 注意· 每个对象都有一个互斥锁标记,用来分配给线程的。· 只有拥有对象互斥锁标记的线程,才能进入对该对象加锁的同步代码块· 线程退出同步代码块时,会释放相应的互斥锁标记。
4.2. 同步方式(2)
4.2.1. 同步方法:
synchronized 返回值类型 方法名称 (形参列表0){
//对当前对象(this)加锁
//代码(原子操作)
}
4.2.2. 注意· 只有拥有对象互斥锁标记的线程,才能进入对该对象加锁的同步代码块· 线程退出同步代码块时,会释放相应的互斥锁标记。
4.3. 同步规则
4.3.1. 只有在调用包含同步代码块的方法,或者同步方式时,才需要对象的锁标记。
4.3.2. 如调用不包含同步代码块的方法,或普通方法时,则不需要标记,可直接调用
5.线程通信
5.1. 等待
public final void wait()
public final void wait(long timeout)
必须在对obj加锁的同步代码块中。在一个线程中,调用obj.wait()时,此线程会释放其拥有的所有锁标记。同时此线程阻塞在o的等待队列中。释放锁,进入等待队列。
5.2. 通知
public final void notify()
public final void notifyAll()
必须在对obj加锁的同步代码中。从obj的Waiting中释放一个或全部线程,对自身没有任何影响。
生产者消费者案例:
一个对多线程理解非常有帮助的一个案例,通过强哥的讲解,每一次练习都有不同的理解。建议在多线程这一章多多练习这个案例。
package t3;
public class TestProductCustomer {
public static void main(String[] args) {
Shop shop = new Shop();
Thread p = new Thread(new Product(shop),"生产者");
Thread c = new Thread(new Customer(shop),"消费者");
p.start();
c.start();
}
}
class Goods{
private int id;
public Goods(int id) {
this.id = id;
}
public int getId() {
return id;
}
public void setId(int id) {
this.id = id;
}
}
class Shop{
Goods goods;
boolean flag;//标识商品是否充足
//生产者调用的 存的方法
public synchronized void saveGoods(Goods goods) throws InterruptedException {
//1.判断商品是否充足
if(flag == true) {
System.out.println("生产者:商品充足!要等待了!"); this.wait();//商品充足,生产者不用生产,而等待消费者买完!进入等待状态
}
//商品不充足!生产者生产商品,存到商场里
System.out.println(Thread.currentThread().getName()+"生产并在商场里存放了"+goods.getId()+"件商品");
this.goods = goods;
flag = true;//已经有商品了!可以让消费者购买了!
//消费者等待。。。
this.notifyAll();//将等待队列的消费者唤醒!前来购买商品
}
//消费者调用的 取的方法
public synchronized void buyGoods() throws InterruptedException {
if(flag == false) {//没有商品了!消费者就要等待!
System.out.println("消费者:商品不充足!要等待了!"); this.wait();
//消费者进入等待队列!等待生产者生产商品后,唤醒!
} //正常购买商品
System.out.println(Thread.currentThread().getName()+"购买了"+goods.getId()+"件商品");
//商品买完了!标识没货了!
this.goods = null;
flag =false;
//唤醒生产者去生产商品
this.notifyAll();
}
}
//生产者
class Product implements Runnable{
Shop shop;//商场
public Product(Shop shop) {
this.shop = shop;
}
public void run() {
//通过循环,生产商品存放到Shop里
for(int i = 1;i<=30;i++) {
try {
//生产者线程调用存商品的方法。传一个商品对象
this.shop.saveGoods(new Goods(i));
} catch (InterruptedException e) {
e.printStackTrace();
}
}
}
}
//消费者
class Customer implements Runnable{
Shop shop;//商场
public Customer(Shop shop) {
this.shop = shop; }
public void run() {
for(int i =1;i<=30;i++) {
try {
this.shop.buyGoods();
} catch (InterruptedException e) {
e.printStackTrace();
}
}
}
}
高级多线程
1.线程池
1.1. 原理
将任务提交给线程池,由线程池分配线程、运行任务,并在当前任务结束后复用线程
1.2. 概念
线程容器,可设定线程分配的数量上限。
将预先创建的线程对象存入池中,并重用线程池中的线程对象
避免频繁的创建和销毁
1.3. 线程池的获取
常用的线程池接口和类
(所在包:java.util.concurrent)· Executor:线程池的顶级接口。· ExecutorService:线程池接口,可通过submit(Runnable task)提交任务代码· 通过newFixedThreadPool(int nThreads)获取固定数量的线程池。参数:指定线程池中线程的数量。· 通过newCachedThreadPool()获得动态数量的线程池,如不够则创建新的,没有上限。
2.Callable接口
public interface Callable<V>{
public V call() throws Exception;
}
Callable具有泛型返回值、可以声明异常。
3. Future接口
3.1. 概念
异步接收ExecutorService.submit()所返回的状态结果,当中包含了call()的返回值。
方法
V get()以阻塞形式等待Future中的异步处理结果(call()的返回值)
4. Lock接口
4.1. 常用方法
void lock()
//获取锁,如果锁被占用了,则等待
boolean tryLock()
//尝试获取锁(成功返回true。失败返回false,不阻塞)
void unlock()
//释放锁
5. 重入锁
5.1. ReentrantReadWriteLock
一种支持一些多读的同步锁,读写分离,可分别分配读锁、写锁。
支持多次分配读锁,使多个读操作可以并发执行
6. Collections体系
6.1. CopyOnWriteArraySet
//写操作 表面使用的是add方法,底层实际是用的CopyOnWriteArrayList的 addIfAbsent()来判断要插入的新值是否存在
CopyOnWriteArraySet aset = new CopyOnWriteArraySet();
6.2. CopyOnWriteArrayList
//写有锁, 读无锁的集合。
CopyOnWriteArrayList alist = new CopyOnWriteArrayList();
6.3. ConcurrentHashMap
ConcurrentHashMap<String, String> ch = new ConcurrentHashMap<String, String>();
//分段锁设计 Segment
//不对整个Map加锁,而是对每个Segment加锁
7. Queue接口
7.1. ConcurrentLinkedQueue
Queue q = new ConcurrentLinkedQueue();
q.poll();//删除表头
7.2. Collections的子接口
表示队列FIFO(First In First Out)
8. BlockingQueue接口
8.1. Queue的子接口,阻塞的队列,增加了两个线程状态为无限期等待的方法
8.2. 方法
void put(E e)//将指定元素插入此队列中,如果没有可用空间,则等待
E take()//获取并移除此队列头部元素,如果没有可用元素,则等待。
8.3. 可用于解决生产者、消费者的问题。
8.4. BlockingQueue bq = new ArrayBlockingQueue(3);
//手动固定队列上限
8.5. BlockingQueue lbq = new LinkedBlockingQueue();
//无界队列 最大有 Integer.MAX_VALUE
总结:
这一周在千锋强哥的指导下,一步步的了解了Java课程中多线程的知识,从多线程到高级多线程,通过讲课一次性理解是不太可能的,后续在通过网上查找一些大神的理解,才慢慢的有了些理解,当然不是老师不行,是自己太笨了,一个案例要多敲两边才能理解,尤其是这种并发的线程,中间的逻辑需要搞清楚,就像春哥说的,要有思想,同时,感谢千锋的老师强哥。