1.多线程
两个概念:
(1)并发:指两个或者多个事件在同一时间段内发送。
(2)并行:指两个或者多个事件在同一时刻发送。
线程与进程:
(1)进程:指的是一个内存中运行的应用程序,每个进程都有一个独立的内存空间,一个应用程序可以同时运行多个进程;进程也是程序的一个之星过程,是系统运行程序的基本单位;系统运行一个程序即是一个进程从创建、运行到消亡的过程。
(2)线程:线程是进程中的一个执行单元,负责当前进程中程序的执行,一个进程中至少有一个线程。一个进程中可以有多个线程。
线程调度两种方法:分时调度和抢占式调度;
1.1多线程示例代码:
//我的线程类 继承线程类
public class MyTread extends Thread {
//重写Thread类中的run方法,设置线程任务
@Override
public void run() {
String name = getName();//获取线程名称,例如Thread-0、Thread-1
//或 Thread.currentThread().getName();
for(int i = 0; i < 20; i++) {
System.out.println("run:"+i);
}
}
}
//测试类
public class Demo {
public static void main(String[] args) {//主线程名称为main
MyTread mt = new MyThread();
mt.start();
for(int i = 0; i < 20; i++) {
System.out.println("main:"+i);
}
}
}
/*
结果是,main是按顺序输出,run也是按顺序输出,但是两个线程会抢夺cpu资源,因此会出现穿插输出的现象。
*/
1.2 多线程原理
JVM执行main方法,找操作系统开辟一条main方法通向cpu的路径,即main线程(主线程),cpu通过这个线程,执行main方法。而当执行到
MyTread mt = new MyTread();
开辟一条通向cpu的新路径用来执行run方法,当mt.start()时,开始执行run方法。
此时,对于cpu而言,就有了两条执行的路径,cpu就有了选择的权力。相当于两个线程在争夺cpu的执行时间,谁争夺到了就执行谁的代码。
1.3 多线程存储理解
首先,要知道,java代码中各种方法是存在栈(方法栈)中;java代码中的对象,是存在堆(堆内存)中。一个方法栈代表一个线程。
当执行main方法时,将main方法压入第一个栈。
(1)对于单线程来说,若在main方法中调用其他方法,则将其他方法压入同一个栈中,以后进先出的原则,先执行完新压入的方法再继续执行main方法。
(2)对于多线程来说,即main方法中创建一个新线程时,就会开辟一个新的栈,在新的栈中执行新线程。这样,cpu就会在两个栈中进行选择执行代码。
其一个明显的好处就是,多个线程之间互不影响。
1.4 线程常用方法
(1)获取线程名称
使用Thread类中的getName()方法获取线程名称;
String getName();
(2)获取当前正在执行的线程对象的引用
static Thread currentThread();
实例见1.1
(3)使得当前正在执行的线程以毫秒数暂停,暂停线程暂停。
public class demo {
public static void main(String[] args) {
//因为sleep是静态方法,因此通过类名调用它
for(int i = 1; i<=60;i++){
System.out.println(i);
try{
Thread.sleep(1000);//即实现每秒输出一次
} catch(InterruptedException e) {
e.printStackTrace();
}
}
}
}
(4)java.lang.Runnable
该方法是另一种创建线程的方式。Runnable接口应该由那些打算通过某一线程执行其实例的类来实现。
类必须定义一个称为run的无参数方法。
创建步骤如下:
1.定义Runnable接口的实现类,并重写该接口的run()方法,该run()方法的方法体同样是该线程的线程执行体。
2.创建Runnable实现类的实例,并以此作为Thread的target来创建Thread对象,该Thread对象才是真正的线程对象。
3.调用线程对象的start()的方法来启动线程。
//Runnable实现类
public class RunnableImp1 implements Runnable {
@Override
public void run() {
for(int i = 0; i < 20; i++) {
System.out.println(Thread.currentThread().getName() + i);
}
}
}
//Runnable实现类
public class RunnableImp2 implements Runnable {
@Override
public void run() {
for(int i = 0; i < 20; i++) {
System.out.println("hello" + i);
}
}
}
//主方法
public class demo {
public static void main(String[] args) {
//创建Runnable接口的实现类对象
RunnableImp1 run = new RunnableImp1();
//创建Thread类对象,构造方法中传递Runnable接口的实现类对象
//Thread t = new Thread(run);
//体现Runnable接口使程序的易扩展性
Thread t = new Thread(new RunnableImp2());//打印hello+i
t.start();
for(int i = 0; i < 20 ; i++) {
System.out.println(Thread.currentThread().getName()+"-->"+i);
}
}
}
/*
同1.1代码效果
*/
(5)Runnable创建多线程相对于Thread创建的好处
Runnable接口创建多线程的好处:
a:避免了单继承的局限性:一个类只能继承一个类,类继承了Thread类就不能继承其他的类。但实现了Runnable接口,还可以继承其他的类,实现其他的接口。
b:增强了程序的扩张性,降低了程序的耦合性(解耦)。实现Runnable接口的方式,把设置线程任务和开启线程进行了分离(解耦)。创建Thread类对象,调用start方法,来开启多线程。
1.5匿名内部类方式实现线程的创建
其匿名内部类的作用是简化代码。主要方法是把子类继承父类,重写父类的方法,创建子类对象一步完成;或者是把实现类接口、重写接口的方法、创建实现类对象合成一步完成。
匿名内部类的最终产物:子类/实现类对象,而这个类没有名字。
格式:
new 父类/接口() {
重写父类/接口中的方法
};
public class Demo {
public static void main(String[] args) {
//线程的父类是Thread
//new MyThread().start();
new Thread(){
@Override
public void run() {
for(int i = 0; i < 20; i++) {
System.out.println(Thread.currentThread().getName()+"-->"+"Hello");
}
}
}.start();
//线程的接口Runnable
//Runnable r = new RunnableImp();
Runnable r = new Runnable() {
@Override
public void run() {
for(int i = 0; i < 20; i++) {
System.out.println(Thread.currentThread().getName()+"-->"+"Hello");
}
}
};
new Thread(r).start();
//或者更简单的嵌套进去
new Thread(new Runnable() {
@Override
public void run() {
for(int i = 0; i < 20; i++) {
System.out.println(Thread.currentThread().getName()+"-->"+"Hello");
}
}
}).start();
}
}
1.6 线程安全问题
以卖票问题举例,若三个窗口在卖同一套的100张票,就会出现问题。比如有两个窗口在卖同一张票,就会出现一张票两次售卖的问题。从多线程的角度来看,即多线程访问了共同的数据,会产生线程安全问题。
代码观察
//定义多个线程共享的票源
public class RunnableImpl implements Runnable {
private int ticket = 100;
//设置线程任务:卖票
@Override
public void run() {
//先判断票是否存在
while(true) {
try {
sleep(10);
} catch (InterruptedException e) {
e.printStackTrace();
}
if(ticket>0) {
System.out.println(Thread.currentThread().getName()+"-----正在卖第"+ticket+"张票);
ticket--;
}
else
break;
}
}
}
//创建三个线程进行测试
public class Demo {
public static void main(String[] args) {
RunnableImpl run = new RunnableImpl();
Thread t0 = new Thread(run);
Thread t1 = new Thread(run);
Thread t2 = new Thread(run);
t0.start();
t1.start();
t2.start();
}
}
1.7 线程安全问题解决
线程安全问题有三种解决方式。
1.7.1 第一种方案:使用同步代码块
synchronized(锁对象) {
可能会出现线程安全问题的代码(访问了共享数据的代码)
}
注意:
1.通过代码块中的对象,可以使用任意的对象;
2.但必须保证多个线程中使用的锁对象是同一个;
3.锁对象的作用是,把同步代码锁住,只让一个线程在同步代码中执行
//锁对象
public class RunnableImpl implements Runnable {
private int ticket = 100;
Object obj = new Object();
//设置线程任务:卖票
@Override
public void run() {
//先判断票是否存在
while(true) {
//同步代码块
synchronized (obj) {
if(ticket>0) {
System.out.println(Thread.currentThread().getName()+"-----正在卖第"+ticket+"张票);
ticket--;
}
else
break;
}
}
}
}
同步技术的原理是,使用了一个锁对象,相当于是对象监视器。
当3个线程一起抢夺cpu的执行权时,谁抢到了就执行run方法进行卖票。
- 假设当t0抢到cpu的执行权,执行run方法,遇到synchronized代码块,这时t0会检查synchronized代码块是否有锁对象,发现有锁对象,该线程就会获取锁对象,然后进入到同步中执行。
- 此时,当t1抢到了cpu的执行权,执行run方法,遇到synchronized代码块,这时t1会检查synchronized代码块是否有锁对象。发现没有锁对象,则t1进入组合状态,会一直等待t0线程执行完同步中的代码,当执行完成,线程会把锁对象归还给同步代码块。此时t1才能获取到锁对象,从而进入到代码块中执行代码。
总结:同步中的线程,没有执行完毕不会释放锁,同步外的线程没有锁进不去同步代码块中,这样就保证了只能有一个线程在同步中执行共享数据,保证了线程安全。但是程序也会因为频繁的判断锁,获取锁,释放锁,而导致程序的效率降低。
1.7.2 第二种方案:使用同步方法(静态和非静态)
使用步骤:
1.把访问了共享数据的代码抽取出来,放到一个方法中;
2.在方法上添加synchronized修饰符
定义方法的格式:
修饰符 synchronized 返回值类型 方法名(参数列表) {
}
//定义多个线程共享的票源
public class RunnableImpl implements Runnable {
private int ticket = 100;
//设置线程任务:卖票
@Override
public void run() {
//先判断票是否存在
while(true) {
payTicket();
}
}
public synchronized void payTicket() {
if(ticket>0) {
System.out.println(Thread.currentThread().getName()+"-----正在卖第"+ticket+"张票);
ticket--;
}
else
break;
}
}
}
注意:定义一个同步方法,同步方法也会把方法内部的代码锁住,只让一个线程执行,同步方法的锁对象就是实现类对象new RunnableImpl(),也就是this。
静态同步方法的锁对象不能this,this时创建对象之后产生的,而静态方法优先于对象。静态方法的锁对象时本类的class属性–>class文件对象。
//定义多个线程共享的票源
public class RunnableImpl implements Runnable {
private int ticket = 100;
//设置线程任务:卖票
@Override
public void run() {
//先判断票是否存在
while(true) {
payTicket();
}
}
public static void payTicket() {
synchronized(RunnableImpl.class) {
if(ticket>0) {
System.out.println(Thread.currentThread().getName()+"-----正在卖第"+ticket+"张票);
ticket--;
}
else
break;
}
}
}
1.7.3 第三种方案:Lock锁
java.util.cuncurrent.lock接口
Lock实现提供了比使用synchronized方法和语句可获得的更广泛的锁定操作。
Lock接口中的方法:
void lock() //获取锁
void unlock() //释放锁
使用步骤:
1.在成员位置创建一个ReentrantLock对象;
2.在可能会出现安全问题的代码前调用Lock接口中的方法lock获取锁;
3.在可能会出现安全问题的代码后调用Lock接口中的方法unlock释放锁;
//定义多个线程共享的票源
public class RunnableImpl implements Runnable {
private int ticket = 100;
//设置线程任务:卖票
//1.在成员位置创建一个ReentrantLock对象
Lock l = new ReentrantLock();
@Override
public void run() {
//先判断票是否存在
while(true) {
//在可能会出现安全问题的代码前调用Lock接口中的方法lock获取锁;
l.lock();
if(ticket>0) {
try {
Thread.sleep(10);
System.out.println(Thread.currentThread().getName()+"-----正在卖第"+ticket+"张票);
ticket--;
} catch(InterruptedException e) {
e.printStackTrace();
} finally {
l.unlock();//无论程序是否异常,都会把锁释放,以提高程序效率。
}
}
}
}
}
1.8 线程状态概述
1.9 线程间的通信
线程间的通信常用到函数wait(), notify(), notifyall().
调用wait和notify方法需要注意的细节:
(1)wait方法和notify方法必须要由同一个锁对象调用。因为:对应的锁对象可以通过notify唤醒使用同一个锁对象的调用的wait方法后的线程。
(2)wait方法与notify方法是属于Object类的方法,因为锁对象可以是任意对象,而任意对象的所属类都是继承类Object类的。
(3)wait方法与notify方法必须要在同步代码块中或者是同步函数中使用。因为必须要通过锁对象调用这两个方法。
/*
等待唤醒机制案例(线程间的通信):线程之间的通信
创建一个顾客线程:告知要的商品数量,调用wait方法,放弃cpu执行,进入WAITING状态;
创建一个老板线程:花5秒做商品,做好后调用notify()方法,唤醒顾客线程拿商品;
注意:
老板和顾客必须使用同步代码块包裹起来,保证等待和唤醒只有一个在执行;
同步使用的锁对象必须保证唯一;
只有锁对象才能调用wait和notify方法;
Object类中方法
void wait()
在其他线程调用此对象的notify()方法或者notifyAll()方法前,使当前线程等待;
void notify()
唤醒在此对象监视器上等待的单个线程;
然后会继续执行wait方法之后的代码;
*/
public calss Demo01WaitAndNotify {
public static void main(String[] args) {
//创建锁对象,保证唯一
Object obj = new Object();
//创建一个顾客线程(消费者)
new Thread(){ //匿名类
@Override
public void run() {
synchronized (obj) {
System.out.println("告诉老板顾客要的商品数量");
//调用wait方法,放弃cpu的执行,进入到WAITING状态
try {
obj.wait();
} catch (InterruptedException e) {
e.printStackTrace();
}
//唤醒之后执行的代码
system.out.println("顾客我拿到商品啦");
}
}
}.start();
new Thread(){
@Override
public void run() {
try {
Thread.sleep(5000);
} catch (InterruptedException e) {
e.printStackTrace();
}
synchronized(obj) {
system.out.println("老板5秒后,告知顾客OK了")
obj.notify();
}
}
}.start();
}
}
1.10 线程间的通信 实例
需求分析:
生产者(包子铺)类:
是一个线程类,可以继承thread;
设置线程任务(run):生产包子;
对包子状态进行判断
true:有包子
- 包子铺调用wait方法进入等待状态
false:没有
- 包子铺生产包子
交替生产两种包子(i%2==0)
包子铺生产好了修改包子状态为true
唤醒吃货线程,吃包子
注意:
1)包子铺线程和吃货线程关系是通信的(互斥);
2)必须同时同步技术保证两个线程只有一个执行;
3)锁对象必须保证唯一,可以使用包子对象作为锁对象;
4)包子铺和吃货的类需要把包子对象作为参数传递进来;
因此需要在成员未知创建一个包子变量
使用带参构造方法,为包子铺变量赋值
消费者(吃货)类,继承Thread
设置线程任务(run):吃包子
对包子状态进行判断:
false:没有包子
吃货调用wait方法进入等待
true:有包子
吃货吃包子,知道吃完
修改包子状态为false
吃货唤醒包子铺线程,生产
public class BaoZi {
String pi;
String xian;
boolean flag = false;
}
public class BaoZiPu extends Thread {
private BaoZi bz;
public BaoZiPu(BaoZi bz) {
this.bz = bz;
}
@Override
public void run() {
int count = 0;
//同步技术
while(true) {
synchronized(bz) {
if(bz.flag == true) {
try {
bz.wait();
} catch (InterruptedException e) {
e.printStackTrace();
}
}
// 被唤醒后执行包子铺生产包子,交替生产两种
if(count%2 == 0) {
//生产三仙包子
bz.pi = "薄皮";
bz.xian = "三鲜馅";
} else {
bz.pi = "冰皮";
bz.xian = "牛肉”;
}
count++;
System.out.println("包子铺正在生产:"+bz.pi+bz.xian+“包子”);
try {
Thread.sleep(3000);
} catch (InterruptedException e) {
e.printStackTrace();
}
//包子生产好,状态为有包子
bz.flag = true;
bz.notify();
System.out.println("包子铺生产好包子");
}
}
}
}
public class ChiHuo extends Thread {
private BaoZi bz;
public ChiHuo(BaoZi bz) {
this.bz = bz;
}
@Override
public void run() {
while(true){
synchronized(bz) {
if(bz.flag == false) {
try {
bz.wait();
} catch (InterruptedException e) {
e.printStackTrace();
}
}
System.out.ptintln("吃货正在吃包子"+bz.pi+bz.xian+"的包子");
bz.flag = false;
bz.notify();
System.out.println("吃货把:"+bz.pi+bz.xian+包子铺开始生产包子");
System.out.println("+++++++++++++++++");
}
}
}
}
public class Demo {
public static void main(String[] args) {
BaoZi bz = new BaoZi();
new BaoZiPu(bz).start();
new ChiHuo(bz).start();
}
}
2. 线程池
如果并发线程数量很多,但每个线程都是执行一个时间很短的任务就结束了,这样频繁创建和销毁线程非常消耗时间。线程池是为了使得线程可以复用,即执行完一个任务,并不销毁,而是可以继续执行其他任务。线程池实际上就是一个容器,LinkedList。
其优点是:
1)降低资源消耗;2)提高响应速度;3)提高线程的可管理性。
线程池图解如下:
2.1 线程池代码实现
线程池是在JDK1.5之后提供的;
java.util.concurrent.Execetors:线程池的工厂类,用来生成线程池
Executors类中的静态方法:
- static ExecutorService newFixedThreadPool(int nThreads) 创建一个可重用的固定线程数的线程池
- 参数:int nThreads:创建线程池中包含的线程数量;
- 返回值是ExecutorService接口,返回的是ExecutorService接口的实现类对象,我们可以使用ExecutorService接口接收(面向接口编程)
java.util.concurrent.ExecutorService:线程池接口
用来从线程池中获取线程,调用start方法,执行线程任务;
submit(Runnable task) //提交一个Runnable任务用于执行
关闭/销毁线程池的方法:void shutdown()
线程池的使用步骤:
1)使用线程池的工厂类Executors里面提供的静态方法newFixedThreadPool生产一个指定线程数量的线程池;
2)创建一个类,实现Runnable接口,重写run方法,设置线程任务;
3)调用ExecutorService中的方法submit,床底线程任务(实现类),开启线程,执行run方法
4)调用ExecutorService中的方法shutdown销毁线程池
public class Demo01ThreadPool {
public static void main(String[] args) {
ExecutorService es = Executors.newFixedThreadPool(2);
es.submit(new RunnableImpl());
}
}
public class RunnableImpl implements Runnables {
@Override
public void run() {
System.out.println(Thread.currentThread().getName()+"创建了一个新的线程");
}
}