线程
想要学习线程就必须了解线程和进程之间的区别
线程和进程之间的区别
-
进程:对于操作系统而言,进程就是程序,多进程就是多个程序同时执行
注意:一个进行可包含一到多个线程,每一个进程都有自己的代码和运行空间,进程之间切换开销较大,进程是资源分配的最小单位。
-
线程:程序中的顺序流,多线程就是一个程序中,多个顺序流在同时执行
注意:一系列线程共享代码和数据空间,每个线程都有自己的程序计数器,线程之间切换开销较小,线程是cpu调度的最小单位
线程和进程的状态:新生->就绪->运行->阻塞->终止
线程的创建方式
1.继承Thread,重写run()方法
- 在run()方法中定义线程体
- 开启:使用start()方法开启线程
package thread01;
//创建线程的方法一:继承Thread类,并重写run()方法
public class ThreadDemo01 extends Thread{
@Override
public void run() {
//定义线程体
for(int i=1;i<=10;i++){
System.out.println("一遍吃饭");
}
}
public static void main(String[] args) {
//1.创建线程体
ThreadDemo01 th=new ThreadDemo01();
//2.开启多线程
th.start();
//错误示例
// th.run(); //请注意:直接调用run()方法,是方法的调用,并不是开启线程
//3.执行同步执行主线程的方法
for(int i=1;i<=10;i++){
System.out.println("一遍玩手机");
}
//运行结果
/* 一遍玩手机
一遍玩手机
一遍玩手机
一遍玩手机
一遍玩手机
一遍玩手机
一遍玩手机
一遍玩手机
一遍玩手机
一遍玩手机
一遍吃饭
一遍吃饭
一遍吃饭
一遍吃饭
一遍吃饭
一遍吃饭
一遍吃饭
一遍吃饭
一遍吃饭
一遍吃饭
从运行结果可以看出,是先运行主方法里面的,在运行线程的
其实两个是同时运行的,但是由于cpu调度和效率的问题
所以会造成 先执行主方法在执行线程的假象
*
*/
}
}
注意:继承Thread()中run()没有抛出异常,所以在它的子类也无法抛出异常。
2.实现Runnable接口,重写run()方法,推荐
- 实现Runnable接口,重写run()方法
- 通过Thread类中的start()开启线程,将实现Runnable的实例当成构建Thread对象的时候的参数
package thread01;
/*创建线程的方法二:
实现Runnable接口,重写run()方法,推荐
开启:通过Thread类中的start()开启线程
优点:1.避免了单继承的局限性 2.实现了资源共享
*/
public class ThreadDemo02 implements Runnable{
//重写方法,当被调用时候,会逐行执行里面的代码
@Override
public void run() {
for(int i=1;i<=10;i++){
System.out.println("一遍刷微博");
}
}
public static void main(String[] args) {
//1.创建线程体
ThreadDemo02 th=new ThreadDemo02();
//2.开启多线程 将实现Runable的实例当成创建Thread对象时候的参数
Thread t=new Thread(th);//因为开启线程的方法在Thread中,Thread作为代理类出现
t.start();
//3.执行同步执行主线程的方法
for(int i=1;i<=10;i++){
System.out.println("一遍看头条");
}
}
/* 运行结果如下
* 一遍看头条
一遍刷微博
一遍刷微博
一遍刷微博
一遍刷微博
一遍刷微博
一遍刷微博
一遍刷微博
一遍刷微博
一遍刷微博
一遍刷微博
一遍看头条
一遍看头条
一遍看头条
一遍看头条
一遍看头条
一遍看头条
一遍看头条
一遍看头条
一遍看头条
*/
}
案例一:模拟12306案例
需求:3个人去买100张火车票
要求:采用多线程的方法,
package thread01;
/*
* 需求:3个人去买100张火车票
* 要求:采用多线程的方法,
*/
//分析:100张票是3个共享的,所以可以放在类成员中,3个人购买说明有3个线程同时在操作
public class Web12306 implements Runnable{
//设置火车票总数
int tikets=100;
//设置线程,我们知道每次只能购买一张,直到最后一张票卖完了才结束
//行为:三个人购买100张票
//在只有一个线程方法的前提下,每次购买一张,每个人调用一次,每人每次购买一张
//线程体的方法为:每次购买一张的行为,直到100张结束
@Override
public void run() {
//由于我们不知道什么时候买到100张,所以可以使用死循环
while(true){
System.out.println(Thread.currentThread().getName()+"购买倒数第"+tikets+"张");
if(tikets<=0){break;}
tikets--;
}
}
public static void main(String[] args) {
Web12306 web=new Web12306();
//由于是三个人同抢100张票,所以是在同一个对象里面操作,不过这个对象有三个线程
Thread th1=new Thread(web,"黄牛一");
Thread th2=new Thread(web,"黄牛二");
Thread th3=new Thread(web,"黄牛三");
//开启三个人同时抢票的模式,就是 开启三个线程
th1.start();
th2.start();
th3.start();
/*运行结果如下:(部分)
* 黄牛一购买倒数第100张
黄牛一购买倒数第99张
黄牛一购买倒数第98张
黄牛一购买倒数第97张
黄牛一购买倒数第96张
黄牛一购买倒数第95张
黄牛一购买倒数第94张
黄牛一购买倒数第93张
黄牛一购买倒数第92张
黄牛一购买倒数第91张
黄牛一购买倒数第90张
黄牛一购买倒数第89张
黄牛一购买倒数第88张
黄牛一购买倒数第87张
黄牛一购买倒数第86张
黄牛一购买倒数第85张
黄牛三购买倒数第100张
黄牛三购买倒数第83张
黄牛三购买倒数第82张
黄牛三购买倒数第81张
黄牛三购买倒数第80张
黄牛三购买倒数第79张
黄牛二购买倒数第100张
通过以上我们可以发现,虽然实现了每次每人抢一次票的功能,
但是有的人抢到同样票,这个是因为线程不安全
*/
}
}
案例二:龟兔赛跑
需求:兔子、乌龟同时在跑,当某一个人跑到100步时,比赛结束,兔子每跑10步休息5ms
package thread01;
/*
* 龟兔赛跑: 兔子,乌龟同时在跑,有人跑到100步游戏结束,兔子跑十步休息5ms
*
* 分析:龟兔同时在赛跑,都是在赛跑,说明是执行同样的方法
* 当跑满100步时,说明已经比赛结束,那么说明需要有标识,提示说明时候结束比赛
* 判断条件:某个跑满100步
*/
public class Race04 implements Runnable{
//3.设置一个标识
boolean flag=false;
//1.龟兔赛跑,每次执行的动作
//2.在这个方法体中,需要一个判断
// 2.1某个跑满100步,给一个标识
// 2.2告诉下一个,上面已经赢了,停止跑步
@Override
public void run() {
int i=1;
//判断条件一,当跑满100步,存储获胜者的名字
while(i<=100){
//如果是乌龟直接跑,但如果是兔子则应跑10步休息5ms
if("兔子".equals(Thread.currentThread().getName())&&(i%10==0)){
try {
Thread.sleep(1);
} catch (InterruptedException e) {
// TODO Auto-generated catch block
e.printStackTrace();
}
}
System.out.println(Thread.currentThread().getName()+"跑了"+i+"步");
i++;
//当跑到100步的时,设置变量flag变为true,其中flag为共享的资源
if(i==100){
flag=true;
System.out.println(Thread.currentThread().getName()+"获胜了");
break;
}
if(flag){
break;
}
}
}
//主方法
public static void main(String[] args) throws InterruptedException {
//1.创建对象
Race04 race=new Race04();
//2.开启线程
//2.1创建Thread对象
Thread tortise=new Thread(race,"乌龟");
Thread rabbit=new Thread(race,"兔子");
//2.2开启线程
tortise.start();
rabbit.start();
}
}
3.实现Callable接口,重写call方法
- 实现Collable接口,重写call()方法,方法中定义线程体
- 开启
- 创建本身对象:就是构建自己的实例
- 创建执行任务:构建一个线程池
- 提交执行:
- 获取结果
package thread01;
import java.util.concurrent.Callable;
import java.util.concurrent.ExecutionException;
import java.util.concurrent.ExecutorService;
import java.util.concurrent.Executors;
import java.util.concurrent.Future;
/*
* 龟兔赛跑: 兔子,乌龟同时在跑,有人跑到100步游戏结束,兔子跑十步休息5ms
*
* 分析:龟兔同时在赛跑,都是在赛跑,说明是执行同样的方法
* 当跑满100步时,说明已经比赛结束,那么说明需要有标识,提示说明时候结束比赛
* 判断条件:某个跑满100步
*/
public class Race05 implements Callable{
//3.设置一个标识
boolean flag=false;
//主方法
public static void main(String[] args) throws InterruptedException, ExecutionException {
//01.创建对象
Race05 race=new Race05();
//02.创建执行任务 设置两个线程
ExecutorService server=Executors.newFixedThreadPool(2);
//03.提交执行
Future result01=server.submit(race);//传入的参数是 对象race
Future result02=server.submit(race);
//04.获取结果
String s1=(String) result01.get();
String s2=(String) result01.get();
//05.打印
System.out.println(s1+"-->"+s2);
}
//1.龟兔赛跑,每次执行的动作
//2.在这个方法体中,需要一个判断
// 2.1某个跑满100步,给一个标识
// 2.2告诉下一个,上面已经赢了,停止跑步
@Override
public Object call() throws Exception {
int i=1;
//判断条件一,当跑满100步,存储获胜者的名字
while(i<=100){
//如果是乌龟直接跑,但如果是兔子则应跑10步休息5ms
if("兔子".equals(Thread.currentThread().getName())&&(i%10==0)){
try {
Thread.sleep(1);
} catch (InterruptedException e) {
// TODO Auto-generated catch block
e.printStackTrace();
}
}
System.out.println(Thread.currentThread().getName()+"跑了"+i+"步");
i++;
//当跑到100步的时,设置变量flag变为true,其中flag为共享的资源
if(i==100){
flag=true;
System.out.println(Thread.currentThread().getName()+"获胜了");
break;
}
if(flag){
break;
}
}
return Thread.currentThread().getName();
}
}
其他的类也同样能够使用线程 如:成员内部类 局部内部类以及匿名内部类
代码如下:
package thread02;
public class Demo01 {
//成员内部类
static class Inner01 implements Runnable{
@Override
public void run() {
for(int i=0;i<10;i++){
//线程休眠10毫秒
try {
Thread.sleep(10);
} catch (InterruptedException e) {
// TODO Auto-generated catch block
e.printStackTrace();
}
System.out.println("成员成员内部类"+i);
}
}
}
public static void main(String[] args) {
//方法内部类
class Inner02 implements Runnable{
@Override
public void run() {
for(int i=0;i<10;i++){
try {
//线程休眠10毫秒
Thread.sleep(10);
} catch (InterruptedException e) {
// TODO Auto-generated catch block
e.printStackTrace();
}
System.out.println("局部成员内部类"+i);
}}
}
//创建对象
Inner01 inner01=new Inner01();
Inner02 inner02=new Inner02();
//创建Thread对象
Thread inner001=new Thread(inner01);
Thread inner002=new Thread(inner02);
//开启线程
inner001.start();
inner002.start();
//匿名内部类
new Thread(new Runnable(){
//匿名内部内的方法
@Override
public void run() {
for(int i=0;i<10;i++){
try {
//线程休眠10毫秒
Thread.sleep(10);
} catch (InterruptedException e) {
// TODO Auto-generated catch block
e.printStackTrace();
}
System.out.println("匿名内部类"+i);
}}
}){}.start();
//正常的方法
for(int i=0;i<10;i++){
try {
//线程休眠10毫秒
Thread.sleep(10);
} catch (InterruptedException e) {
// TODO Auto-generated catch block
e.printStackTrace();
}
System.out.println("正常方法"+i);
}
}
}
线程的状态问题
状态 | 解释 |
---|---|
NEW | 启动 |
RUNNABLE | 可运行线程的线程状态。 |
BLCOKED | 受阻塞并且正在等待监视器锁的某一线程的线程状态。 |
WAITING | 某一等待线程的线程状态。 |
TIMED_WAITING | 具有指定等待时间的某一等待线程的线程状态。 |
TERMINATED | 已终止线程的线程状态。 |
线程的状态过程
- 新生状态:new
- 就绪状态:start(),线程就会进入到就绪队列,等待cpu的调度
- 运行状态:
- 阻塞状态:非正常执行完毕,通过程序控制
- 终止状态:
注意:一个线程一旦进入到终止状态,没有办法恢复了,就算是重写一个new 线程,也不是刚才那个线程了。
一个线程一旦进入到阻塞状态,无法直接恢复到运行,等待阻塞解除之后,恢复到就绪状态。
为什么不是到运行状态呢?这个是因为要等待cpu的调度。
如何进入到就绪状态
- 正常的开始 start()
- 阻塞状态解除
- 线程切换,被切换的线程就进入到就绪状态
- yield()礼让线程
如何进入到阻塞状态
- sleep()方法 线程休眠
- 可以模拟网络延迟
- 放大问题的可能性
- 也叫做抱着资源睡觉,同步的对象资源,让出cpu资源
- join()方法
- wait()方法
如何让线程进入到终止状态
- 正常执行完毕
- destorty()|stop() 已过时
- 通过标识手动判断-建议
案例一:模拟网络延迟 (进入到阻塞状态 sleep())
package thread02;
public class StateDemo02 implements Runnable{
public static void main(String[] args) {
new Thread(new StateDemo02()).start();;
}
//模拟倒计时
@Override
public void run() {
for(int i=10;i>=0;i--){
try {
Thread.sleep(1000);
} catch (InterruptedException e) {
// TODO Auto-generated catch block
e.printStackTrace();
}
System.out.println("倒计时:"+i);
}
}
}
案例二:进入到就绪状态 yield
package thread02;
public class YieldDemo03 implements Runnable {
@Override
public void run() {
System.out.println(Thread.currentThread().getName()+"start");
//每次执行到该行时候,让出资源
Thread.yield();//静态方法 礼让线程
System.out.println(Thread.currentThread().getName()+"end");
}
public static void main(String[] args) {
new Thread(new YieldDemo03(),"我是A的").start();
new Thread(new YieldDemo03(),"我是B的").start();
}
}
案例三:合并线程
package thread02;
/*
*/
public class JoinDemo05 {
public static void main(String[] args) {
new Thread(new Father()).start();
}
}
//自定义类 Father
class Father implements Runnable{
@Override
public void run() {
System.out.println("1.Father want somking");
System.out.println("2.give money to son");
Thread th=new Thread(new Son());
//父亲先开始
th.start();
//加入儿子
try {
th.join();
} catch (InterruptedException e) {
// TODO Auto-generated catch block
e.printStackTrace();
}
System.out.println("8.somking");
}
}
线程安全
多个线程同时操作同一个资源的时候,才有可能出现线程安全问题
通过同步synchronized关键字控制线程安全
同步方法:静态方法和成员方法
同步块:synchronized(列|this|资源|代码)
this,锁住当前对象
类名:类名.class 一个类的class对象,一个类只有一个Class对象
几个方法的优缺点
- 锁方法简单,但是效率低,因为锁的代码范围太大的整个方法,如果想要提高效率,可以同步块
- 同步类,相当于把这个类的所有对象全部同步了,如果想要只同步当前对象就可以实现,可以同步this
- 同步this,同步当前对象,锁住这个对象,这个对象的所有资源都被锁住了,如果只想要锁住其中某个资源,可以只锁这个资源
- 同步资源,一般指成员变量
- 锁:要锁不变的东西,自定义引用数据类型的对象地址永远不变
注意
锁,要锁住不变的东西,会变的锁不住
同步的范围太大,效率低,同步的范围太小,锁不住
案例一:锁住当前对象this
package practice05;
public class JDS{
public static void main(String[] args) {
//1.创建对象
SyncThread syncThread = new SyncThread();
//2.创建线程 sycThread是对象
Thread thread1 = new Thread(syncThread, "SyncThread1");
Thread thread2 = new Thread(syncThread, "SyncThread2");
//3.开启线程
thread1.start();
thread2.start();
}
}
/**
* 同步线程
*/
class SyncThread implements Runnable {
private static int count;
public SyncThread() {
count = 0;
}
public void run() {
//this,锁住当前对象
synchronized(this)
{
for (int i = 0; i < 5; i++) {
try {
System.out.println(Thread.currentThread().getName() + ":" + (count++));
Thread.sleep(100);
} catch (InterruptedException e) {
e.printStackTrace();
}
}
}
}
public int getCount() {
return count;
}
}
当两个并发线程(thread1和thread2)访问同一个对象(syncThread)中的synchronized代码块时,在同一时刻只能有一个线程得到执行,另一个线程受阻塞,必须等待当前线程执行完这个代码块以后才能执行该代码块。Thread1和thread2是互斥的,因为在执行synchronized代码块时会锁定当前的对象,只有执行完该代码块才能释放该对象锁,下一个线程才能执行并锁定该对象。