多线程概述
一、多线程的概念
多线程是指程序中包含多个执行单元,即在一个程序中可以同时运行多个不同的线程来执行不同的任务,也就是说允许单个程序创建多个并行 。
二、使用多线程的情况
- 程序需要同时执行两个或多个任务
- 程序需要实现一些需要等待的任务时,如用户输入、文件读写操作、网络操作、搜索等。比如用户输入,当用户输入部分占据CPU时,如果一直没有输入,不可能让这一部分一直占据CPU,这个时候会让别的线程上CPU运行
- 需要一些后台运行的程序时
二、多线程的优缺点(重在缺点)
优点:
-
提高程序的响应
-
提高CPU的利用率
-
改善程序结构,将复杂任务分为对个线程,独立运行
缺点:
- 线程也是程序,所以线程需要占用内存,线程越多占用内存也越多
第一点容易改善,我们不断提高电脑的性能就可以很好的改善占用内存的问题
-
多线程需要协调和管理,所以需要CPU时间跟踪线程
-
线程之间对共享资源的访问会相互影响,必须解决竞用共享资源的问题(最主要的问题)
比如我们春节期间买电影票,班里30个人买同一电影院的同一场次,电影票作为一个共享的资源,张三想买7排8号,恰巧李四也想买7排8号。张三和李四他俩购票的时候相当于创建了两个线程(多线程),如果不加以控制,他俩竞争共享资源,张三刚买完票,票数计数器还没来得及减一就进入阻塞状态,而李四看到的这张票还在,他也买了。这个时候就会出现事故。这是多线程存在的最大问题。
三、并行和并发
理解并行和并发要注意到两个词:同一时刻和一个时间段
并行:并行是在同一时刻发生多件事情。例如:多个运动员听到枪响那一时刻同时起跑、多个CPU同一时刻开始执行多个线程
并发:并发是在一个时间段内,多个事情发生,这几个事件在时间上很紧凑,但还是有先后顺序的。例如:一个单核CPU一次只能执行一个线程,但是我们一边放音乐,一边用QQ聊天看起来是同时发生的,但其实是不断交换着进行的。由于这些线程上下处理机的时间非常短暂,在我们看来他们是同时发生的,这就是并发。
线程同步
一、多线程同步
*** 多个线程同时读写同一份共享资源时,可能会引起冲突(上面买电影票的例子)。所以引入线程“同步”机制,即各线程间要有先来后到;
同步就是排队+锁:
-
几个线程之间要排队,一个个对共享资源进行操作,而不是同时进行操作
-
为了保证数据在方法中被访问时的正确性,在访问时加入锁机制
二、模拟买票为例讲解线程同步
有两个窗口卖票,当前的总票数是10张,分别用继承Thread和实现Runnable两种方式实现:
一、继承Thread方式
创建包:package com.ffyc.javathread.demo6;
创建类:TicketThread类
创建类:Test类
public class TicketThread extends Thread{
int num = 10; //设定有10张票
static Object obj = new Object();
@Override
public void run(){
while(true){
synchronized(obj) { //进入同步代码块就会枷锁-->将对象头中的锁状态改为枷锁
if (num > 0) {
try {
Thread.sleep(100);
} catch (InterruptedException e) {
e.printStackTrace();
}
System.out.println(Thread.currentThread().getName() + "买到了票:" + num);
num--;
} else {
break;
}
} //同步代码块执行完后会自动释放锁
}
}
}
/*
synchronized(同步对象) 同步对象可以是任意类的对象,但是只能是唯一的,只有一份的
这种情况不能用this,因为我们创建两个线程对象,哪一个线程访问,this就表示哪一个线程
对象中有一个区域叫对象头,对象头中有一个锁状态的记录
*/
上面的代码让我详细讲解,看不懂,不可能!
public class TicketThread extends Thread{
int num = 10;//num表示车票 作为类的成员属性==整体的变量
@Override
public void run(){
/*线程创建看上一篇文章,无论是继承Thread,还是实现Runnable接口,都要重写run()方法,run()方法的方 法体写的是线程要执行的任务*/
}
}
先重写出run()方法的轮廓就可以,具体实现完后看
***我们要在main方法中创建出题目要求的两个窗口(线程)
创建包:package com.ffyc.javathread.demo6;
创建类:Test类
package com.ffyc.javathread.demo6;
public class Test{
public static void main(String [] args){
TicketThread th1 = new TicketThread();
/*创建了一个线程对象,也就是创建了一个线程*/
th1.setName("窗口1"); //Thread中给线程命名的方发:setName()
th1.start();
/* 启动线程,也就是-->进入就绪状态,等待CPU的调度*/
//创建第二个线程
TicketThread th2 = new TicketThread();
th2.setName("窗口2");
th2.start();
}
}
接下来我们来实现run()方法:
@Override
public void run(){
while(true){
if(num>0){
//买票方法体
System.out.println(Thread.currentThread().getName()+"买到票:"+mun);
num--;
}else{
break;//当mun票数小于等于0就说明没票了,break结束
}
}
}
现在,整合起来就是:
package com.ffyc.javathread.demo6;
public class TicketThread extends Thread{
int num = 10;//num表示车票 作为类的成员属性==整体的变量
@Override
public void run(){
/*线程创建看上一篇文章,无论是继承Thread,还是实现Runnable接口,都要重写run()方法,run()方法的方 法体写的是线程要执行的任务*/
while(true){
if(num>0){
//买票方法体
System.out.println(Thread.currentThread().getName()+"买到票:"+mun);
//sleep()方法需要try/catch异常
//sleep()方法是线程休眠参数100指的是休眠100ms,即让当前正在执行的线程休眠(暂停执行)
try{
Thread.sleep(100);
}catch(InterruptedException e){
e.printStackTrace();
}
num--;
}else{
break;//当mun票数小于等于0就说明没票了,break结束
}
}
}
}
package com.ffyc.javathread.demo6;
public class Test{
public static void main(String [] args){
TicketThread th1 = new TicketThread();
/*创建了一个线程对象,也就是创建了一个线程*/
th1.setName("窗口1"); //Thread中给线程命名的方发:setName()
th1.start();
/* 启动线程,也就是-->进入就绪状态,等待CPU的调度*/
//创建第二个线程
TicketThread th2 = new TicketThread();
th2.setName("窗口2");
th2.start();
}
}
上面这个卖票程序会出错,不是编译期的错误,是程序运行时会出现上面举的买电影票例子的情况:窗口1卖出去了7排8号的票,窗口2也卖出去了这张票。
同时买到了第10张票,同时买到了第8张票…总共10张票却卖出去了12张。
多线程存在的这个问题我们可以通过排队+加锁来处理,即线程同步机制。
加锁的两种方式(这里先说给代码块加锁,给方法加锁见下一篇)
- 用synchronized(同步对象)关键字加锁
用synchronized(同步对象)可以给方法加锁,也可以给代码块加锁:
(1)给代码块加锁:
package com.ffyc.javathread.demo6;
public class TicketThread extends Thread{
int num = 10;//num表示车票 作为类的成员属性==整体的变量
Object obj = new Object();
@Override
public void run(){
/*线程创建看上一篇文章,无论是继承Thread,还是实现Runnable接口,都要重写run()方法,run()方法的方 法体写的是线程要执行的任务*/
while(true){
synchronized(obj){
if(num>0){
//买票方法体
System.out.println(Thread.currentThread().getName()+"买到票:"+mun);
//sleep()方法需要try/catch异常
//sleep()方法是线程休眠参数100指的是休眠100ms,即让当前正在执行的线程休眠(暂停执行)
try{
Thread.sleep(100);
}catch(InterruptedException e){
e.printStackTrace();
}
num--;
}else{
break;//当mun票数小于等于0就说明没票了,break结束
}
}
}
}
}
synchronized(同步对象) 中的同步对象 可以是任意类的对象,但是只能是唯一的,只有一份的。
想明白synchronized关键字怎么加锁,需要了解一点对象的底层:
其实,对象中有一个区域叫对象头,对象头中有一个锁状态的记录,我们在TicketThread类中,创建了一个Object对象,当两个线程中的一个进入锁之后obj对象的锁状态会标记锁里面有线程,此时别的线程就无法进来,直到synchronized(obj){}执行完,释放标志位 。一次只允许一个线程进入,是安全的。
这种情况不能用this,因为我们创建了两个线程对象(th1和th2),在访问锁里面的代码时:哪一个线程访问,this就表示 哪一个线程的对象,两个线程,就有两个this对象,不唯一,导致都可以进入锁。
以下是运行结果:
这篇文章就是理解线程同步,后面部分请见下一篇~