线程学习(二)
案例:模拟售票员售票,四个窗口各卖100张票。
1.线程的实现
1.1线程创建方式有三种:
1 继承Thread类
2 实现Runnable接口
3 实现Callable接口(了解即可,此处不进行介绍)
1.2通过继承Thread类实现案例要求
class SellTickets extends Thread {
//数据
int ticket = 100;
@Override
public void run() {
//循环售票
while(true) {
if(ticket<1){
break;
}
System.out.println(Thread.currentThread().getName() + "售出了第"+ticket+"张票");
ticket--;
}
}
}
public static void main(String[] args) {
//需求:模拟4个售票员各售100张票
SellTickets w1 = new SellTickets();
SellTickets w2 = new SellTickets();
SellTickets w3 = new SellTickets();
SellTickets w4 = new SellTickets();
w1.start();
w2.start();
w3.start();
w4.start();
}
1.3Thread类实现步骤:
继承自Thread类,Thread类是所有线程类的父类,实现了对线程的抽取和封装
继承Thread类创建并启动多线程的步骤:
1.定义一个类,继承自Thread类,并重写该类的run方法,该run方法的方法体就代表了线程需要完成的任务,因此,run方法的方法体被称为线程执行体
2.创建Thread子类的对象,即创建了子线程
3.用线程对象的start方法来启动该线程
注意:
1 程序运行时会自动创建一个线程 ,这个线程叫主线程;可以通过主线程创建子线程。
2 启动线程使用start()方法,不要直接调用run()方法。
1.4通过实现Runnable接口实现案例要求
//票
public class TicketRes implements Runnable{
int ticket = 100;
@Override
public void run() {
while(true) {
if(ticket<1){
break;
}
System.out.println(Thread.currentThread().getName() + "售出了第"+ticket+"张票");
ticket--;
}
}
}
public class ThreadTextDemo02 {
public static void main(String[] args) {
// 票
TicketRes res=new TicketRes();
//线程对象
Thread t0 = new Thread(res);
Thread t1 = new Thread(res);
Thread t2 = new Thread(res);
Thread t3 = new Thread(res);
t0.start();
t1.start();
t2.start();
t3.start();
}
}
1.5Runnable接口 实现步骤
实现Runnable接口创建并启动多线程的步骤:
1.定义一个Runnable接口的实现类,并重写该接口中的run方法,该run方法的方法体同样是该线程的线程执行体
2.创建Runnable实现类的实例,并以此实例作为Thread的target来创建Thread对象,该Thread对象才是真正的线程对象
3.调用线程对象的start方法来启动该线程
1.6两种实现方式的比较
继承Thread类的方式:
1.没有资源共享,编写简单
如果要访问当前线程,除了可以通过Thread.currentThread()方式之外,还可以使用getName()获取线程名字。
2.弊端:因为线程类已经继承了Thread类,则不能再继承其他类【单继承】
实现Runnable接口的方式
1.可以多个线程共享同一个资源,所以非常适合多个线程来处理同一份资源的情况
2.资源类实现了Runnable接口。如果资源类有多个操作,需要把功能提出来,单独实现Runnable接口。
3.弊端:编程稍微复杂,不直观,如果要访问当前线程,必须使用Thread.currentThread()
总结:实际上大多数的多线程应用都可以采用实现Runnable接口的方式来实现【推荐使用匿名内部类】
2.多线程访问临界资源
2.1 多线程访问临界资源时的数据安全问题
临界资源 :多个线程同时访问的资源。
产生原因:有多个线程在同时访问一个资源,如果一个线程在取值的过程中,时间片又被其他线程抢走了,临界资源问题就产生了
2.2 解决临界资源问题
一个线程在访问临界资源的时候,如果给这个资源“上一把锁”,这个时候如果其他线程也要访问这个资源,就得在“锁”外面等待。
2.3同步代码块
同步:Synchronized:有等待
异步:Asynchronized:没有等待,各执行各的
语法:
synchronized(锁) {
//需要访问临界资源的代码段
}
说明:
1.程序走到代码段中,就用锁来锁住了临界资源,这个时候,其他线程不能执行代码段中的代码,只能在锁外边等待
2.执行完代码段中的这段代码,会自动解锁。然后剩下的其他线程开始争抢cpu时间片
3.一定要保证不同的线程看到的是同一把锁,否则同步代码块没有意义
2.3.1同步代码块使用
public class Ticket implements Runnable{
// 需求:100张
// 临界资源
private int ticket = 100;
@Override
public void run() {
while (true) {
//上锁
synchronized(this){
if (ticket < 1) {
break;
}
System.out.println("售票员" + Thread.currentThread().getName() + "售出第"+ticket+"张票");
ticket--;
}
}
}
}
2.4 同步方法
2.4.1 同步非静态方法
public class Ticket implements Runnable{
// 需求:100张
// 临界资源
private int ticket = 100;
@Override
public void run() {
while (true) {
if(!sale()){
break;
}
}
}
public synchronized boolean sale(){//锁是this
if (ticket < 1) {
return false;
}
System.out.println("售票员" + Thread.currentThread().getName() + "售出第"+ticket+"张票");
ticket--;
return true;
}
}
2.4.2 同步静态方法
public class Ticket implements Runnable{
// 需求:100张
// 临界资源
private static int ticket = 100;
@Override
public void run() {
while (true) {
if(!sale()){
break;
}
}
}
public synchronized static boolean sale(){ //锁是 类.class
if (ticket < 1) {
return false;
}
System.out.println("售票员" + Thread.currentThread().getName() + "售出第"+ticket+"张票");
ticket--;
return true;
}
}
2.5ReentrantLock类(可重入锁)jdk1.5
从jdk1.5之后加入新的接口 Lock,ReentrantLock是Lock接口的实现类。
通过显式定义同步锁对象来实现同步,同步锁提供了比synchronized代码块更广泛的锁定操
注意:最好将 unlock的操作放到finally块中
通过使用ReentrantLock这个类来进行锁的操作,它实现了Lock接口,使用ReentrantLock可以显式地加锁、释放锁。
public static void main(String[] args) {
Ticket res=new Ticket();
Thread t0 = new Thread(res, "喜羊羊");
Thread t1 = new Thread(res, "沸羊羊");
Thread t2 = new Thread(res, "灰太狼");
Thread t3 = new Thread(res, "小灰灰");
t0.start();
t1.start();
t2.start();
t3.start();
}
//票类
public class Ticket implements Runnable{
// 需求:100张
// 临界资源
private int ticket = 100;
// 定义一个ReentrantLock类的对象
ReentrantLock lock = new ReentrantLock();
@Override
public void run() {
while (true) {
//上锁
lock.lock();
if (ticket < 1) {
break;
}
System.out.println("售票员" + Thread.currentThread().getName() + "售出第"+ticket+"张票");
ticket--;
//解锁
// unlock()
lock.unlock();
//注意:lock()和unlock()都是成对出现的
}
}
}
3.线程池
3.1创建线程池的原因
当有非常多的任务需要多线程来完成,且每个线程执行时间不会太长,这样会频繁的创建和销毁线程。频繁创建和销毁线程会比较耗性能。如果有了线程池就不要创建更多的线程来完成任务,因为线程可以重用。
线程池用维护者一个队列,队列中保存着处于等待(空闲)状态的线程。不用每次都创建新的线程。
3.2线程池的接口
1 Executor:线程池的核心接口,负责线程的创建使用和调度的根接口
2 ExecutorService: Executor的子接口,线程池的主要接口, 提供基本功能。
3 ScheduledExecutorService: ExecutorService的子接口,负责线程调度的子接口。
3.3线程池的实现类
1 ThreadPoolExecutor:ExecutorService的实现类,负责线程池的创建使用。
2 ScheduledThreadPoolExecutor:继承 ThreadPoolExecutor,并实现 ScheduledExecutorService接口,既有线程池的功能,又具有线程调度功能。
3 Executors:线程池的工具类,负责线程池的创建。
newFixedThreadPool();创建固定大小的线程池。
newCachedThreadPool();创建缓存线程池,线程池大小没有限制。根据需求自动调整线程数量。
newSingleThreadExecutor();创建单个线程的线程池,只有一个线程。
newScheduledThreadPool();创建固定大小的线程池,可以延迟或定时执行任务。
3.4使用线程池实现案例
public class Ticket implements Runnable{
private int ticket=100;
@Override
public void run() {
while(true) {
if(ticket<=0) {
break;
}
System.out.println(Thread.currentThread().getName()+"卖第"+ticket+"张票");
ticket--;
}
}
}
public static void main(String[] args) {
Ticket ticket=new Ticket();
ExecutorService threadPool = Executors.newFixedThreadPool(4);
for(int i=0;i<4;i++) {
threadPool.submit(ticket);
}
threadPool.shutdown();
System.out.println("主线程执行完毕........");
}