多线程
1.引言
计算机的配置越来越高,CPU计算速度越来越快,如果能够将一个单一执行的程序进行优化,分解成多个小块(几乎同时)进行,能够大大提高开发效率,对CPU的利用率也能够提升一布。
2.进程和线程
进程:进程是执行程序的一个过程,一个程序从打开到关闭算做一个进程。进程是一个动态的过程,每个进程都有自己独立的一块内存空间,有自己的生存周期。
线程:进程中的一个执行任务(控制单元),负责当前进程中程序的执行。一个进程至少有一个线程,一个进程可以运行多个线程,多个线程可共享数据。与进程不同的是同类的多个线程共享进程的堆和方法区资源,但每个线程有自己的程序计数器、虚拟机栈和本地方法栈,所以系统在产生一个线程,或是在各个线程之间作切换工作时,负担要比进程小得多,也正因为如此,线程也被称为轻量级进程。
并发和并行
并发:发生在同一时间间隔内,即理解为一个人同时做很多事
并行:发生在同一时刻,即理解为不同的人同时在做不同的事
3.线程的基本API
1.start():1.启动当前线程2.调用线程中的run方法
2.run():通常需要重写Thread类中的此方法,将创建的线程要执行的操作声明在此方法中
3.currentThread():静态方法,返回执行当前代码的线程
4.getName():获取当前线程的名字
5.setName():设置当前线程的名字
6.yield():主动释放当前线程的执行权
7.join():在线程中插入执行另一个线程,该线程被阻塞,直到插入执行的线程完全执行完毕以后,该线程才继续执行下去
8.stop():过时方法。当执行此方法时,强制结束当前线程。
9.sleep(long millitime):线程休眠一段时间
10.isAlive():判断当前线程是否存活
4.线程的创建方式
1.继承Thread抽象类
创建一个线程的第二种方法是创建一个新的类,该类继承 Thread 类,然后创建一个该类的实例。
继承类必须重写 run() 方法,该方法是新线程的入口点。它也必须调用 start() 方法才能执行。
2.继承Runnable接口
实现Runnable接口,重写run方法
3.继承Callable接口
-
- 创建 Callable 接口的实现类,并实现 call() 方法,该 call() 方法将作为线程执行体,并且有返回值。
-
- 创建 Callable 实现类的实例,使用 FutureTask 类来包装 Callable 对象,该 FutureTask 对象封装了该 Callable 对象的 call() 方法的返回值。
-
- 使用 FutureTask 对象作为 Thread 对象的 target 创建并启动新线程。
-
- 调用 FutureTask 对象的 get() 方法来获得子线程执行结束后的返回值。
范例:线程基本创建
/**
* Runnable接口
*/
public class ThreadDemo{
public static void main(String[] args) {
Lock lock = new ReentrantLock();
Thread t = new Thread(new Runnable() {
@Override
public void run() {
System.out.println("我是谁");
}
});
t.start();
}
}
线程池
背景:经常创建和销毁,使用量特别大的资源,比如并发情况下的线程,对性能影响很大。
思路:提前创建好多个线程,放入线程池之,使用时直接获取,使用完放回池中。可以避免频繁创建销毁,实现重复利用。类似生活中的公共交通工具。(数据库连接池)
好处:提高响应速度(减少了创建新线程的时间)
降低资源消耗(重复利用线程池中线程,不需要每次都创建)
便于线程管理
ExecutorService 和 Executors
ExecutorService:真正的线程池接口。常见子类ThreadPoolExecutor.
void execute(Runnable coommand):执行任务/命令,没有返回值,一般用来执行Runnable
Futuresubmit(Callable task):执行任务,有返回值,一般又来执行Callable
void shutdown():关闭连接池。
Executors 工具类,线程池的工厂类,用于创建并返回不同类型的线程池
Executors.newCachedThreadPool() 创建一个可根据需要创建新线程的线程池
Executors.newFixedThreadPool(n) 创建一个可重用固定线程数的线程池
Executors.newSingleThreadExecutor() :创建一个只有一个线程的线程池
Executors.newScheduledThreadPool(n) 创建一个线程池,它可安排在给定延迟后运行命令或者定期地执行。
范例:创建一个定长线程池
/**
*定长线程池
* (长度为指定数值)
* 任务加入后的执行流程:
* 1.判断线程池中是否有空闲线程
* 2.存在则使用
* 3.不存在且线程池未满,则添加新的线程进入线程池,并执行
* 4.不存在且线程池已满,则排队等待空闲线程
*/
public class DemoFixedThreadPool {
public static void main(String[] args) {
ExecutorService executorService = Executors.newFixedThreadPool(5);
executorService.execute(new Thread(new Runnable() {
@Override
public void run() {
System.out.println(Thread.currentThread().getName()+"锄禾");
}
}));
}
}
范例:创建一个缓存线程池
/**
*缓存线程池
* (长度没有限制)
* 任务加入后执行流程:
* 1.判断线程池中是否又空闲线程
* 2.有则使用
* 3.没有则创建新的线程添加到线程池,然后使用
*/
public class DemoCachedThreadPool {
public static void main(String[] args) throws InterruptedException {
ExecutorService executorService = Executors.newCachedThreadPool();
executorService.execute(new Thread(new Runnable() {
@Override
public void run() {
System.out.println(Thread.currentThread().getName()+"楚河");
}
}));
}
}
范例:创建一个周期线程池
/**
* 周期任务 定长线程池
* 执行流程:
* 1.判断线程是否为空闲线程
* 2.存在则使用
* 3.不存在空闲线程,且线程池未满的情况下,则创建线程并放入线程池,然后使用
* 4.不存在空闲线程,且线程池以满的情况下,则等待线程池存在空闲线程
*/
public class Demo周期线程池 {
public static void main(String[] args) {
ScheduledExecutorService executorService = Executors.newScheduledThreadPool(2);
executorService.scheduleAtFixedRate(new Runnable() {
@Override
public void run() {
System.out.println(Thread.currentThread().getName()+"楚河");
}
},5,1, TimeUnit.SECONDS);
executorService.scheduleAtFixedRate(new Runnable() {
@Override
public void run() {
System.out.println(Thread.currentThread().getName()+"楚河");
}
},5, 1,TimeUnit.SECONDS);
executorService.scheduleAtFixedRate(new Runnable() {
@Override
public void run() {
System.out.println(Thread.currentThread().getName()+"楚河");
}
},5, 1,TimeUnit.SECONDS);
}
}
5.线程安全
创建三个窗口卖票,总票数为100张票
1.卖票过程中,出现了重票(票被反复的卖出,ticket未被减少时就打印出了)错票。
2.问题出现的原因:当某个线程操作车票的过程中,尚未完成操作时,其他线程参与进来,也来操作车票。(将此过程的代码看作一个区域,当有线程进去时,装锁,不让别的线程进去)
因此,多个线程对同一个共享数据进行操作时,线程没来得及更新共享数据,从而导致另外线程没得到最新的数据,从而产生线程安全问题。
Java中,有三种方式解决这个问题:
5.1同步代码块
同步监视器(俗称:锁):任何一个的对象都可以充当锁。(但是为了可读性一般设置英文成lock)当锁住以后只能有一个线程能进去(要求:多个线程必须要共用同一把锁,比如火车上的厕所,同一个标志表示有人)
范例:
/**
* 解决线程不安全,同步代码块
*/
public class Demo1 {
public static void main(String[] args) {
Runnable ticket = new Ticket();
new Thread(ticket).start();
new Thread(ticket).start();
new Thread(ticket).start();
}
static class Ticket implements Runnable{
private int count = 10;
Object o = new Object();//创建一个Object类型的变量,充当锁
@Override
public void run() {
while (true){//通过锁使得线程排队完成,获得锁的线程继续走,没有获得锁的线程等待
synchronized (o) {
if (count>0) {
System.out.println("正在卖票");
try {
Thread.sleep(1000);
} catch (InterruptedException e) {
e.printStackTrace();
}
count--;
System.out.println(Thread.currentThread().getName() + "出票成功,当前票数剩余" + count);
}else {
break;
}
}
}
}
}
}
5.2同步方法
用同步方法,对方法进行synchronized关键字修饰
将同步代码块提取出来成为一个方法,用synchronized关键字修饰此方法。
对于runnable接口实现多线程,只需要将同步方法用synchronized修饰
而对于继承自Thread方式,需要将同步方法用static和synchronized修饰,因为对象不唯一(锁不唯一)
/**
* 同步方法
*/
public class Demo2 {
public static void main(String[] args) {
Runnable ticket = new Ticket();
new Thread(ticket).start();
new Thread(ticket).start();
new Thread(ticket).start();
}
public static class Ticket implements Runnable{
private int count = 10;
@Override
public void run() {
while (true){
sale();
}
}
private synchronized void sale(){
if (count>0) {
System.out.println("正在卖票");
try {
Thread.sleep(1000);
} catch (InterruptedException e) {
e.printStackTrace();
}
count--;
System.out.println(Thread.currentThread().getName()+"出票成功,当前票数剩余"+count);
}
}
}
}
5.3显式锁
实际定义出来一个锁。
/**
* 显式锁
*/
public class Demo3 {
public static void main(String[] args) {
Runnable ticket = new Ticket();
new Thread(ticket).start();
new Thread(ticket).start();
new Thread(ticket).start();
}
public static class Ticket implements Runnable{
private int count = 10;
Lock lock = new ReentrantLock();
@Override
public void run() {
while (true) {
lock.lock();
if (count > 0) {
System.out.println("正在卖票");
try {
Thread.sleep(1000);
} catch (InterruptedException e) {
e.printStackTrace();
}
count--;
System.out.println(Thread.currentThread().getName() + "出票成功,当前票数剩余" + count);
}
lock.unlock();
}
}
}
}
总结:显式锁Lock和隐式锁synchronized的区别?
1.获取和释放操作:
Lock需要进行实例,获取操作和释放操作,而synchronized不需要任何获取和释放的操作,只需要加上修饰符即可
2.Lock是通过API来获取的,而synchronized是由JVM来维护
3.Lock可以通过lock和unlock随时管理开始和中断,而synchronized则不能中断
4.Lcok可以通过赋值,true为公平锁,false为非公平锁,而synchronized则固定为非公平锁
值得一提的是,公平锁和非公平锁,举个例子,高中下课所有人跑步到食堂买饭,先到的人买了一份饭,而后面的人在等待,这个时候先到的那个人的同学发来了一条信息,然后先到的同学又买了一份,后面的同学只能再等,这就是非公平,而公平就是买完之后大家重新回到教室再次跑步到食堂买饭,先到先得。
优先使用顺序:
LOCK-同步代码块-同步方法
判断线程是否有安全问题,以及如何解决:
1.先判断是否多线程
2.再判断是否有共享数据
3.是否并发的对共享数据进行操作
4.选择上述三种方法解决线程安全问题