线程
- 进程:正在执行中的程序。每一个进程执行,都有一个执行的顺序,该顺序就是一个执行路径,或者叫一个控制单元。
- 线程:就是进程中的一个独立的控制单元,线程在控制着进程的执行。
简单一点来说进程和线程的关系,打开任务管理器可以看到很多正在执行的程序,每一个正在执行的程序就是进程,
而比如说迅雷下载数据的时候,会开辟很多条请求去找服务端请求数据(一条请求下载120%,另一条请求下载2140%…这样可以提高效率),而这些开辟的请求就是线程。
一个进程中至少有一个线程。
例如:
public class ThreadDemo {
public static void main(String[] args)
{
for (int x=0;x<800 ;x++ )
{
System.out.println("Hello World");
}
}
}
- 在该程序运行的时候,Java虚拟机会启动,这个时候就会多一个进程java.exe.
- 该进程中至少有一个线程负责Java程序的运行,而且这个线程运行的代码存在于main方法中,该线程称之为主线程。
- 其实要是深追究的话,该程序运行的时候,不止一个线程,还有负责垃圾回收机制的线程,
多线程存在的意义:
- 可以使在运行时有多个程序同时运行的效果。
- 多条线程运行同一个程序,提高了程序运行的效率。
创建线程
- 如何自定义一个线程呢?
步骤:- 定义类继承Thread
- 复写Thread中的run方法(将自定义的代码存储在run方法中,让线程运行)。
- 调用线程中的start方法该方法有两个作用:启动线程和调用run方法。
示例:运行一下代码,
//创建线程
class Demo extends Thread
{
public void run()
{
for (int x=0;x<70 ;x++ )
{
System.out.println("demo run---"+x);
}
}
}
public class ThreadDemo2 {
public static void main(String[] args)
{
Demo d = new Demo();
d.start();
for (int x=0;x<70 ;x++ )
{
System.out.println("Hello World......."+x);
}
}
}
会出现图中的结果:
由运行结果可以看到,程序执行过程中,自定义线程和主线程共同抢夺CPU资源,运行流程图如下:
- 发现运行的结果每次都不一样,这是因为多个线程都在获取CPU的执行权,CPU执行到谁,谁就运行
- 明确一点,在某一时刻,只能有一个程序在运行(多核除外),CPU在做着快速的切换,以达到看上去是同时运行的效果。
- 我们可以形象的把多线程的运行行为理解为线程在互相抢夺CPU的执行权。
- 这就是多线程的一个特性:随机性。谁抢到谁执行,至于执行多长时间,CPU说了算。
为什么要覆盖run方法呢?
- Thread类用于描述线程。
- 该类定义了一个功能,用于存储线程要运行的代码,该存储功能就是run方法。
- 也就是说Thread类中的run方法,用于存储线程要运行的代码。
简短的一个小练习:
//创建两个线程,和主线程交替执行
//那么我先定义一个类继承Thread
class Test extends Thread
{
private String name;//为了区分线程1和线程2,给线程一个自己特有的标识
Test(String name)
{
this.name=name;
}
public void run()//1,固有格式:先覆盖run方法,在run方法中写上线程要执行的代码
{
for (int x=0;x<60 ;x++ )
{
System.out.println(name+"test run----"+x);
}
}
}
public class ThreadTest {
public static void main(String[] args)
{
Test t1 = new Test("one");
Test t2 = new Test("two");//2、建立线程
t1.start();//3、启动线程
t2.start();
for (int x =0;x<60 ;x++ )//4、主函数执行的代码
{
System.out.println("main run---------"+x);
}
}
}
运行结果部分截图:
- 线程都是有自己默认的名称的:就是Thread_编号,该编号从0开始。
- 那么既然有自己默认的名称,这个名称也是可以自己进行修改的。设置名称的两种方法:
- this.setName();
- super(name);(因为Thread类中有构造函数直接可以自定义线程名称)
- 获取当前线程对象:
- this
- static Thread currentThread();(这个用的比较多一点)两个其实返回的是同一个对象。其实用法就是Thread.currentThread();
通过一段小程序来练习一下线程名称的获取:
//创建两个线程,和主线程交替执行
//然后显示出线程的默认名称,
class Test extends Thread
{
public void run()
{
for (int x=0;x<60 ;x++ )
{
System.out.println(this.getName()/*获取名称*/+"===run----"+x);
}
}
}
public class ThreadDemo3 {
public static void main(String[] args)
{
Test t1 = new Test();
Test t2 = new Test();
t1.start();
t2.start();
for (int x =0;x<60 ;x++ )
{
System.out.println("main run---------"+x);
}
}
}
运行结果为:
发现在默认情况下,每个线程都有自己的名称,名称有自己的编号,编号从0开始。
接下来自定义线程的名称:
//创建两个线程,和主线程交替执行
//自定义线程的名称
class Test extends Thread
{
Test(String name)
{
super(name);
}
public void run()
{
for (int x=0;x<60 ;x++ )
{
System.out.println(Thread.currentThread().getName()+"===run----"+x);
//获取当前线程对象。
}
}
}
public class ThreadDemo4 {
public static void main(String[] args)
{
Test t1 = new Test("one");
Test t2 = new Test("two");
t1.start();
t2.start();
for (int x =0;x<60 ;x++ )
{
System.out.println("main run---------"+x);
}
}
}
运行结果为:
线程的五种状态:
- 被创建:已经被创建,但是没有启用的线程。
- 运行:已经被创建,而且正在执行的线程(有执行资格,有执行权)
- 临时状态:有执行资格但是没有执行权(CPU在某一个时间只能执行一个线程,而这个时候在等候的其他可执行线程就处在临时状态上)
- 冻结:当线程遇到sleep(time),wait指令的时候,会进入冻结状态,(没有执行资格,也没有执行权,但是该线程没有挂掉)什么时候醒呢?等到sleep时间到,或者wait遇上notify(唤醒),该线程就会变为临时状态,有了执行资格,等待CPU的执行权。
- 消亡:当run方法执行完,或者遇到stop方法,表明该线程挂掉了。
通过模拟售票系统来理解线程的应用:
//火车站售票的例子:
class Ticket extends Thread
{
//因为售票系统需要开启多线程,但是票数是固定不变的,所以需要这些线程共享一个数据,所以就把票数定义成静态的
private static int count =100;
public void run()
{
while (true)
{
if (count>0)
{
System.out.println(Thread.currentThread().getName()+"---"+count--);
}
}
}
}
public class TicketDemo {
public static void main(String[] args)
{
//这里假设开启四个窗口售票:
Ticket t1 = new Ticket();
Ticket t2 = new Ticket();
Ticket t3 = new Ticket();
Ticket t4 = new Ticket();
t1.start();
t2.start();
t3.start();
t4.start();
}
}
运行结果:
由以上代码中,我们可以看出,在定义票的总数的时候,为了让多个线程共享同一个数据,将数据定义成了静态的,这样做的缺点是生命周期太长了,一直到类没有数据才会被清空。所以引出了第二种创建线程的方法。
创建线程的第二种方法:
- 1,定义类实现Runnable接口
- 2,覆盖Runnable接口中的run方法
- 将线程要运行的代码存放在该run方法中
- 3,通过Thread类建立线程对象。
- 4,将Runnable接口的子类对象作为实际参数传给Thread类的构造函数。
- 为什么要将Runnable接口的子类对象传递给Thread的构造函数呢? 因为自定义的run方法所属的对象是Runnable接口的子类对象,,所以要让线程去指定对象的run方法,就必须明确该run方法所属对象。
- 5,调用Thread类的start方法开启线程,并调用Runnable接口子类中的run方法。
实例2:
//火车站售票的例子:
//创建线程的第二种方式:
class Ticket implements Runnable
{
//这里无需定义成静态的
private int count =100;
//同样的要覆盖run方法
public void run()
{
while (true)
{
if (count>0)
{
System.out.println(Thread.currentThread().getName()+"---"+count--);
}
}
}
}
public class TicketDemo {
public static void main(String[] args)
{
//创建一个Runnable子类对象。
//再创建一个Thread对象,将Runnable子类对象作为实际参数传给Thread类
//开启线程:
Ticket c = new Ticket();
Thread t1 = new Thread(c);
Thread t2 = new Thread(c);
Thread t3 = new Thread(c);
Thread t4 = new Thread(c);
t1.start();
t2.start();
t3.start();
t4.start();
}
}
运行结果:
实现方式和继承方式有什么区别呢?
- 实现的好处:避免了单继承的局限性。
- 在定义线程时,建议使用实现方式。
- 继承Thread:线程代码存放Thread子类run方法中。
- 实现Runnable:线程代码存在接口的子类run方法中。
通过分析发现,多线程运行出现了安全问题。
- 问题的原因是:当多条语句在操作同一个线程共享数据时,一个线程对多条语句只执行了一部分们还没有执行完,另一个线程参与进来执行,导致了共享数据的错误。
- 解决办法:对多条操作共享数据的语句,只能让一个线程都执行完,在执行过程中,其他线程不可以参与执行。
- Java对于多线程的安全问题提供了专业的解决方式。就是同步代码块。synchronized(对象) { 需要被同步的代码 }
这里我们用sleep来模拟CPU切换出去的效果,再次执行以上的代码:
//火车站售票的例子:
//创建线程的第二种方式:
class Ticket implements Runnable
{
//这里无需定义成静态的
private int count =100;
//同样的要覆盖run方法
public void run()
{
while (true)
{
if (count>0)
{
try
{
Thread.sleep(10);//线程刚判断完进来就睡着了,以此来模拟CPU切换的效果。
}
catch (InterruptedException e)//中断异常之后再进行详细的解说
{
//这里为了简单起见就不进行处理了。
}
System.out.println(Thread.currentThread().getName()+"---"+count--);
}
}
}
}
public class TicketDemo {
public static void main(String[] args)
{
//创建一个Runnable子类对象。
//再创建一个Thread对象,将Runnable子类对象作为实际参数传给Thread类
//开启线程:
Ticket c = new Ticket();
Thread t1 = new Thread(c);
Thread t2 = new Thread(c);
Thread t3 = new Thread(c);
Thread t4 = new Thread(c);
t1.start();
t2.start();
t3.start();
t4.start();
}
}
运行结果是:
运行结果显示,这里还真的出现了安全问题。要怎么解决呢?
上边提到了,用Java中的同步代码块进行解决。
修改方案如下:
//火车站售票的例子:
//解决多线程安全问题,用同步代码块:
class Ticket implements Runnable
{
private int count =100;
Object obj = new Object();
public void run()
{
while (true)
{
//同步代码块是要在外边加一个锁,只要一个线程进来了,在他没有执行完同步代码块中的内容之前,
//其他线程都进不来,
//这个例子就好比火车上的厕所,前一个人出不来,后一个人就别想要进去。
//用同步代码块需要一个对象参数,在这里哪一类型的对象都可以,
//需要注意的是,同步代码块扩住的范围是操作共有数据的(count)部分都要扩起来
synchronized(obj)
{
if (count>0)
{
try
{
Thread.sleep(10);
}
catch (InterruptedException e)
{
//这里为了简单起见就不进行处理了。
}
System.out.println(Thread.currentThread().getName()+"---"+count--);
}
}
}
}
}
public class TicketDemo {
public static void main(String[] args)
{
//创建一个Runnable子类对象。
//再创建一个Thread对象,将Runnable子类对象作为实际参数传给Thread类
//开启线程:
Ticket c = new Ticket();
Thread t1 = new Thread(c);
Thread t2 = new Thread(c);
Thread t3 = new Thread(c);
Thread t4 = new Thread(c);
t1.start();
t2.start();
t3.start();
t4.start();
}
}
运行结果:
同步代码块:
synchronized(对象)
{
//需要被同步的代码
}
对象如同锁。只有锁的线程才可以在同步中执行。
没有持有锁的线程及时获取CPU执行权,也进不去,因为没有获取锁。
经典的实例说明:火车上的卫生间。
同步的前提:
- 必须要有两个或两个以上的线程。
- 必须是多个线程使用同一个锁。
必须保证同步中只有一个线程在运行。
好处:解决了多线程的安全问题。
弊端:多个线程都需要判断锁,较为消耗资源,
练习:银行存款的例子:
//银行储户存钱的例子:两个用户同时过来存钱,两人都存300,每次存100 共存3次
//希望用多线程;
/*
思路:银行只有一个,同时里边会有一个和,表示银行里现在一共有多少钱了。
银行还有存钱方法,add
储户可以 有多个,这里可以开启多线程,储户调用add方法进行存钱,
*/
/*
步骤:1 先描述类,运行程序
2 看哪里有多线程产生的安全问题,进行处理
如何找问题:
1,明确哪些代码是多线程运行代码
2,明确共享数据
3,明确多线程运行代码中哪些语句是操作共享数据的。
*/
class Bank
{
private int sum;
public void add(int n)//add方法中有两句代码,分析问题可能发生的情况。
{
sum = sum+n;
try{Thread.sleep(10);}catch(Exception e){}
//假如线程在这里发生了CPU切换(用sleep方法模拟切换出去的动作),线程0进来sum值变成100-->跳出
//线程2现在进来,因为是共享数据,将sum 的值变为200-->跳出
//这个时候线程0回来了,打印sum值得时候一看是200,就打印出来了,
//这就出现了两次200的情况。
System.out.println(Thread.currentThread().getName()+"*****"+sum);
}
}
class Chus implements Runnable
{
Bank b = new Bank();
public void run()//这里是多线程运行的代码,每一个线程都会有一次循环,而且只有一句执行语句,没有安全问题
{
for (int x=0;x<3 ; x++)
{
b.add(100);//线程运行代码中调用到了add方法,
}
}
}
public class BankDemo {
public static void main(String[] args)
{
Chus c = new Chus();
Thread t1 = new Thread(c);
Thread t2 = new Thread(c);
t1.start();
t2.start();
}
}
因为该程序有安全问题,所以运行结果如下:
所以需要同步代码块进行处理:
//处理过之后的代码
class Bank
{
private int sum;
Object obj = new Object();
public void add(int n)
{
synchronized(obj)
{
sum = sum+n;
try{Thread.sleep(10);}catch(Exception e){}
System.out.println(Thread.currentThread().getName()+"*****"+sum);
}
}
}
class Chus implements Runnable
{
Bank b = new Bank();
public void run()
{
for (int x=0;x<3 ; x++)
{
b.add(100);
}
}
}
public class BankDemo {
public static void main(String[] args)
{
Chus c = new Chus();
Thread t1 = new Thread(c);
Thread t2 = new Thread(c);
t1.start();
t2.start();
}
}
运行结果发现:
发现同步代码块和函数都有一个功能就是封装代码,所以我们可以将同步代码块改写为同步函数的形式,这样就不用创建obj对象了。修改后的代码如下:
class Bank
{
private int sum;
//Object obj = new Object();
public synchronized void add(int n)
{
//synchronized(obj)
{
sum = sum+n;
try{Thread.sleep(10);}catch(Exception e){}
System.out.println(Thread.currentThread().getName()+"*****"+sum);
}
}
}
那么既然学习了用同步函数的方法,也可以讲之前火车票的例子用同步函数的方法进行优化代码:
//火车站售票的例子:
//解决多线程安全问题,用同步代码块:
//通过上一个案例的学习,我们可以用到同步函数:
class Ticket implements Runnable
{
private int count =1000;
public void run()
{
while (true)
{
show();
}
}
public synchronized void show()
{
if (count>0)
{
try
{
Thread.sleep(10);
}
catch (InterruptedException e)
{
}
System.out.println(Thread.currentThread().getName()+"---"+count--);
}
}
}
public class TicketDemo {
public static void main(String[] args)
{
Ticket c = new Ticket();
Thread t1 = new Thread(c);
Thread t2 = new Thread(c);
Thread t3 = new Thread(c);
Thread t4 = new Thread(c);
t1.start();
t2.start();
t3.start();
t4.start();
}
}
那么问题也就来了,同步代码块变成同步函数之后,那个用来当锁的对象没有了,那么同步函数用到的是什么锁呢?
函数需要被调用,那么函数都有一个所属对象引用,就是this
所以同步函数使用的锁是this,
说到这里回顾一下单例设计模式中的懒汉式,因为懒汉式中也用到了同步
//单例设计模式
/*
饿汉式
class Single
{
private static final Single s = new Single();
private Single(){}
public static Single getInstance()
{
return s;
}
}
*/
/*
懒汉式
class Single
{
private static Single s =null;
private Single(){}
public static Single getInstance()
{
if(s==null)
{
syschronized(Single.class)
{
if(s==null)
{
s=new Single();
}
}
}
return s;
}
}
注意:
- 懒汉式和饿汉式有什么区别:懒汉式的特点在于实例的延迟加载;
- 懒汉式延迟加载有没有问题?有,多线程执行的时候会有安全问题。
- 怎么解决?用同步的方式。
- 加同步有哪些方法?用同步代码块和同步函数的方法,但是稍微有一些低效,可以使用双重判断的方法来稍微的提高效率。
- 加同步的时候使用的锁是哪一个?该类所属的字节码文件。(不是this,因为静态中不可能有this)
死锁
什么是死锁?
就像是两个人都只有一只筷子,但是谁都不给谁自己的筷子,这样两个人就吃不上饭了,这就是发生了死锁。
死锁的产生原因?
同步发生了嵌套,就是锁里边还有锁。
死锁事例代码:
//死锁演示:就是锁里边嵌套锁
class Test implements Runnable
{
private boolean flag;
Test(boolean flag)//给要创建的线程对象先初始化一个布尔型的值
{
this.flag = flag;
}
public void run()
{
if (flag)//如果为真,先进a锁,再进b锁
{
while(true)
{
synchronized(MyLock.a)
{
System.out.println("locka");
synchronized(MyLock.b)
{
System.out.println("lockb");
}
}
}
}
else//如果为假,先进b锁,再进a锁
{
while(true)
{
synchronized(MyLock.b)
{
System.out.println("lockb");
synchronized(MyLock.a)
{
System.out.println("locka");
}
}
}
}
}
}
class MyLock
{
static Object a = new Object();
static Object b = new Object();
}
//如果线程0进了a锁,CPU切换,同时
//线程1进了b锁,CPU切换,这时线程0重新夺回执行权的时候,想要b锁,但是b锁被线程1占用着,
//所以两边都不放资源,程序无法进行下去,形成死锁。
public class DeadLockTest {
public static void main(String[] args)
{
Thread t1 = new Thread(new Test(true));
Thread t2 = new Thread(new Test(false));
t1.start();
t2.start();
}
}
运行结果: