Java学习心得(多线程)

本文详细介绍了Java多线程的概念,包括进程、线程的创建、线程安全、同步机制、线程通信、高级多线程技术如线程池、Callable、Future、Lock接口以及阻塞队列等,并通过实例深入理解线程同步和通信。

摘要生成于 C知道 ,由 DeepSeek-R1 满血版支持, 前往体验 >

多线程

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课程中多线程的知识,从多线程到高级多线程,通过讲课一次性理解是不太可能的,后续在通过网上查找一些大神的理解,才慢慢的有了些理解,当然不是老师不行,是自己太笨了,一个案例要多敲两边才能理解,尤其是这种并发的线程,中间的逻辑需要搞清楚,就像春哥说的,要有思想,同时,感谢千锋的老师强哥。

评论
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

当前余额3.43前往充值 >
需支付:10.00
成就一亿技术人!
领取后你会自动成为博主和红包主的粉丝 规则
hope_wisdom
发出的红包
实付
使用余额支付
点击重新获取
扫码支付
钱包余额 0

抵扣说明:

1.余额是钱包充值的虚拟货币,按照1:1的比例进行支付金额的抵扣。
2.余额无法直接购买下载,可以购买VIP、付费专栏及课程。

余额充值