多线程
一、线程实现方式
1.1并发与并行
并发:指两个或多个时间在同一个时间段内发生
并行:指两个或多个时间内在同一时刻发生(同时发生)
1.2线程与进程
进程:指一个内存中运行的程序(由硬盘到内存,占用内存执行)
线程:线程是进程中的执行单元,一个进程至少有一个线程
1.3多线程好处
1.效率高
2.多个线程互不影响
1.4线程调度
1.分时调度
2.抢占式调度(java是抢占式调度)
二、创建线程
2.1创建Thread类的子类
(1)步骤
-
创建一个Thread类的子类
-
写run方法
-
创建Thread类的子类对象
-
调用start方法
JVM执行main方法,找OS开辟一条main方法通向CPU的路径,这个路径叫main线程,主线程,CPU通过这个线程,这个路径可以执行main方法
执行RUN方法时,开辟一条通向CPU的新路径用来执行run方法
结果是随机性的,线程随机调用(抢占式调度)
(2)获得线程名称
Thread.currentThread().getName()
(3)设置线程名称
public MyThread(String name) {
super(name);
}
MyThread mt = new MyThread("旺财");
MyThread mt = new MyThread();
mt.setName("分线程");
(4)sleep方法
Thread.sleep(1000);//暂停1秒(1000毫秒)
2.2继承Runnable接口
(1)步骤
-
创建一个Runnable接口的实现类
-
实现该类,然后放入Thread的构造
-
Thread的start
(2)好处
避免了Thread继承的局限,增强扩展性(解耦),尽量使用这种方式创建线程
(3)匿名内部类
匿名:没有名字
内部类:写在其他类内部的类
匿名内部类作用:简化diamante
匿名内部类最终产物:子类/实现类对象,而这个类没有名字
/**
* 匿名内部类
*/
new Thread() {
@Override public void run() {
String name = Thread.currentThread().getName();
for (int i = 1; i <= 20; i++) {
System.out.println(name + "=>" + i + "汤汤");
}
}
}.start();
/**
* 简化接口方式
*/
new Thread(new Runnable() {
@Override
public void run() {
String name = Thread.currentThread().getName();
for (int i = 1; i <= 20; i++) {
System.out.println(name + "=>" + i + "宝宝");
}
}
}).start();
三、线程安全
3.1售票重复问题
(1)安全问题
多个窗口售票重复的解决(线程安全)
多线程访问了共享数据
当程序出现延时时,就会出现问题
代码:
/**
* 卖票案例 线程类
*/
public class RunnableImpl implements Runnable {
//定义一个多线程共享票源
private int ticket = 1;
//设定线程任务:卖票
@Override
public void run() {
String name = Thread.currentThread().getName();
while (ticket > 0) {
try {
Thread.sleep(10);
}catch (InterruptedException e){
e.printStackTrace();
}
System.out.println(name + "==>正在卖第" + ticket-- + "张票");
}
}
}
/**
* 开启三个线程卖票
*/
public class Demo01Ticket {
public static void main(String[] args) {
Runnable run = new RunnableImpl();
new Thread(run).start();
new Thread(run).start();
new Thread(run).start();
}
}
(2)出现原因
开启多个线程,多个线程一起抢夺CPU的执行权,谁抢到谁执行
当while(ticket>0)判断过后,线程失去了对ticket的实时校验(失去了CPU的执行权),就造成了安全问题
(3)解决办法
线程安全问题是不能产生的,我们可以让一个线程在访问共享数据的时候,无论是否失去了CPU的执行权,让其他的线程只能等待,等待当前线程完成
1.同步代码块
2.同步方法。
3.锁机制
3.2同步代码块
synchronized(锁对象){
可能会出现线程安全的代码
}
注意:
1.同步代码块中的锁对象可以是任意对象
2.但是必须保证多个线程使用的锁对象是用一个
3.锁对象作用:把同步代码块锁住,只让一个线程在同步代码块中执行
总结:
同步中的线程,没有执行完不会释放锁。同步外的线程,没有锁,进不到同步
3.3.1同步方法
(1)使用步骤
-
把访问共享数据的代码抽取出来,放到一个方法中
-
给方法加synchronized
(2)实现方法
同步方法也会把方法内部的代码锁住
只让一个线程执行
同步方法的锁对象是谁?
就是实现类对象 new RunnableImpl()
也就是this
public synchronized void payTicket(String name){
if (ticket > 0) {
try {
Thread.sleep(10);
} catch (InterruptedException e) {
e.printStackTrace();
}
System.out.println(name + "==>正在卖第" + ticket-- + "张票");
}
}
3.3.2静态同步方法
锁对象是谁?
不能是this,因为this是创建对象后产生的,静态方法优先于对象
静态方法的所对象是本类的class属性==》class文件对象(反射)
3.4 Lock方法
java.util.concurrent.locks.look接口
java.util.concurrent.locks.ReentrantLock 实现类
使用步骤:
1.在成员位置创建一个ReentrantLockd 对象
2.在可能出现安全问题的代码前调用Lock接口中的方法lock获取锁
3.在可能出现安全问题的代码后调用Lock接口中的方法unlock释放锁
public class RunnableImpl implements Runnable {
//定义一个多线程共享票源
private int ticket = 100;
Lock l = new ReentrantLock();
//设定线程任务:卖票
@Override
public void run() {
String name = Thread.currentThread().getName();
//创建同步代码块
while (ticket > 0) {
l.lock();
if (ticket > 0) {
try {
try {
Thread.sleep(10);
} catch (InterruptedException e) {
e.printStackTrace();
}
System.out.println(name + "==>正在卖第" + ticket-- + "张票");
} catch (Exception e) {
e.printStackTrace();
} finally {
l.unlock();
}
}
}
}
}
四、线程状态
4.1 状态
新建状态:new
运行状态:start()
阻塞状态:
死亡状态:stop()
休眠(睡眠)状态:sleep(long) wait(long)
无限(永久)等待状态Object.wait(); 唤醒:Object.notify()
4.2 等待唤醒案例
/**
* 等待唤醒案例:线程之间的通信
* 创建一个顾客线程(消费者):告知老板要的包子数量和种类,调用wite()方法,放弃CPU的执行,进入Waiting状态
* 创建一个老板线程(生产者):话了五秒做包子,做好包子之后,调用notify方法唤醒顾客拿包子
* 注意:
* 顾客和老板线程必须使用同步代码块包裹起来,保证等待和唤醒只能有一个正在执行
* 只有所对象才能调用wait和notify
*
* Object类中的方法
* wait()
* 导致当前线程等待,直到另一个线程调用该对象的 notify()方法或 notifyAll()方法。
* notify()
* 唤醒正在等待对象监视器的单个线程。
*/
public class WaitAndNotify {
public static void main(String[] args) {
Object obj = new Object();
new Thread(){
@Override
public void run() {
synchronized (obj){
//进入waiting状态
try {
System.out.println("客户要了包子");
obj.wait();
} catch (InterruptedException e) {
e.printStackTrace();
}
System.out.println("客户拿到包子回家");
}
}
}.start();
new Thread(){
@Override
public void run() {
synchronized (obj){
try {
System.out.println("老板开始做包子");
Thread.sleep(5000);
} catch (InterruptedException e) {
e.printStackTrace();
}finally {
System.out.println("老板做好了包子");
obj.notify();
}
}
}
}.start();
}
}
五、等待唤醒机制
5.1线程间通信
多个线程在锤同一个资源,但是用的作案工具各不相同
为何要线程通信?
我们希望多个线程锤同一个资源时,能够讲道理守规矩,让他们通信,知道这个资源有人锤了
如何保证线程间通信有效利用资源?
5.2等待唤醒机制
重点:
有效利用资源
通信方法:
wait:等待
notify:随机唤醒一个
notifyAll:唤醒所有
注意:
就算被唤醒了,如果CPU被占用,一样不能马上执行,会进入阻塞状态,抢夺CPU资源
细节:
1、wait和notify必须由同一个锁调用。
2、wait和notify是属于Object类的方法。
3、wait和notify方法必须要在同步代码块或同步线程中使用。
六、线程池
我们使用线程的时候就去创建一个线程,这样实现起来非常简单,但是存在问题:
如果并发的线程数量很多,并且每个线程都是执行一个时间很短的任务就结束,这样频繁差UN个点线程就会大大降低系统的效率,因为频繁常见线程和销毁线程需要时间。
线程池就是为了解决这个问题而诞生的
6.1底层原理
线程池:容器–>集合(ArrayList, Hashset, LinkedList, HashMap)
当程序第一次启动的时候,创建多个线程,保存到一个集合中
当我们想要使用线程的时候,就可以从集合中取出来线程使用
Thread t = list.remove(0);
Thread t = linked.removeFirst();
当我们使用完毕线程,需要把线程归还给线程池
list.add(t);
linked.addLast(t);
JDK1.5已经内置类线程池
6.2用法
java.util.concurrent.Executors java.util.concurrent.ExecutorService
public static void main(String[] args) {
ExecutorService es = Executors.newFixedThreadPool(2);
es.submit(new Runnable() {
@Override
public void run() {
System.out.println(Thread.currentThread().getName());
}
});
//线程池会一直开启,使用完了后归还给线程池
es.submit(new RunnableImpl());
es.submit(new RunnableImpl());
es.submit(new RunnableImpl());
es.shutdown();
}
public class RunnableImpl implements Runnable {
@Override
public void run() {
System.out.println(Thread.currentThread().getName()+"创建了一个新的线程");
}
}