——- android培训、java培训、期待与您交流! ———-
java基础之多线程
一. 多线程的概念
1. 进程、线程和多线程:
- 进程:正在进行中的程序(直译)。进程是程序在某个数据集上的运行,它有自己的生命周期,因调度而运行。因等待资源或事件而被处于等待状态,因完成任务而被撤销。
线程:进程中一个负责程序执行的控制单元(执行路径)。是一个程序中的不同执行路径。
- 一个进程中可以有多个执行路径,称之为多线程。
- 一个进程中至少要有一个线程。
- 开启多个线程是为了同时运行多部分代码,每一个线程都有自己运行的内容,这个内容可以称为线程要执行的任务。
- 多线程解决了多部分代码同时运行的问题。
知识扩展:其实更细节说明虚拟机,jvm不只一个线程 还有负责垃圾回收机制的线程
—————————-割——————————-割——————————–
2. 创建线程的两种方法
方法1: 继承Thread类
通过对api的查找 java已经提供了堆线程这类事物的描述 就是Thread类
- 定义类 继承Thread
- 复写Thread类中的run方法
目的是将自定义的代码存储在run方法中,让线程运 - 调用线程的start方法
两个作用:启动线程 调用run方法
如下面的示例代码:
//定义类 继承Thread
class Demo extends Thread
{
//复写run方法
public void run()
{
for(int i = 0;i<60;i++)
System.out.println("Demo run--"+i);
}
}
class ThreadDemo1
{
public static void main(String[] args)
{
Demo d = new Demo();//创建好一个线程
d.start();//启动线程并执行该线程的run方法
for(int x = 0;x<60;x++)
System.out.println("Hello run--"+x);
}
}
从上面的实例代码运行结果可以得到几点:
- 运行结果每一次都不同
- 这是因为多个线程都获取cpu的执行权,cpu执行到谁,谁就运行
- 明确一点,在某个时刻,只能有一个程序在运行
- cpu在作者快速的切换,以达到看上去是同时运行的效果
这就是java多线程的一个特性: 随机性,谁抢到是执行, 至于执行时间多长 cpu说了算.
小知识点:
- 为什么要覆盖run方法呢?
Thread 类用于描述线程,该类定义了一个功能用于存储线程要运行的代码,该存储功能就是run方法
也就是说Thread类中的run方法,用于存储要运行的代码 - 不能直接调用run()方法,因为这样不能开启你线程.仅仅是普通的调用.
—————————-割——————————-割——————————–
方法2: 实现runnable接口
使用继承方式有一个弊端,那就是如果该类本来就继承了其他父类,那么就无法通过Thread类来创建线程了。这样就有了第二种创建线程的方式:实现Runnable接口,并复写其中run方法的方式。
- 定义类实现Runnable接口.
- 覆盖接口中的run方法,将线程的任务代码封装到run方法中
- 通过Thread类创建线程对象,并将Runnable接口的子类对象作为Thread类的构造函数的参数进行传递。
- 调用线程对象的start方法开启线程。
为什么要将Runnable接口的子类对象作为Thread类的构造函数的参数进行传递?
- 因为线程的任务都封装在Runnable接口子类对象的run方法中。 所以要在线程对象创建时就必须明确要运行的任务
示例代码:
//定义类,实现Runnable接口
class Demo implements Runnable
{
public void run()//覆盖run方法
{
show();
}
public void show()
{
for(int x = 0; x < 20; x++)
{
System.out.println(Thread.currentThread().getName() + "..." + x);
}
}
}
class ThreadDemo2
{
public static void main(String[] args)
{
Demo d = new Demo();//创建子类对象
//将Runnable接口的子类对象作为Thread类的构造函数的参数进行传递
Thread t1 = new Thread(d);
Thread t2 = new Thread(d);
t1.start();//启动线程并调用其中的run方法
t2.start();
}
}
运行结果如下:
—————————-割——————————-割——————————–
实现Runnable接口的好处:
- 将线程的任务从线程的子类中分离出来,进行了单独的封装,按照面向对象的思想将任务封装成对象。
- 避免了Java单继承的局限性。 所以,创建线程的第二种方式较为常用。
两种方式的区别
继承方式: 线程代码存放在Thread子类run方法中
实现方式: 线程代码存放在接口的子类的run方法中
—————————-割——————————-割——————————–
3. 线程的生命周期
线程的几种状态:
- 被创建:等待启动,调用start启动。
- 运行状态:具有执行资格和执行权。
- 临时状态(阻塞):有执行资格,但是没有执行权。
- 冻结状态:遇到sleep(time)方法和wait()方法时,失去执行资格和执行权,sleep方法时间到或者调用notify()方法时,获得执行格变为临时状态。
- 消忙状态:stop()方法,或者run方法结束。
如下图:
—————————-割——————————-割——————————–
4. 线程的安全
/*
需求:卖票程序
多个卖票窗口
*/
class Ticket implements Runnable
{
private int tick = 100;
public void run()
{
while(true)
{
if(tick>0)
{
//显示余票
System.out.println(Thread.currentThread().getName()+"---"+tick--);
}
}
}
}
class TicketDemo
{
public static void main(String[] args)
{
//创建Runnable子类的实例对象
Ticket t = new Ticket();
//创建四个线程,模拟四个卖票窗口
Thread t1 = new Thread(t);
Thread t2 = new Thread(t);
Thread t3 = new Thread(t);
Thread t4 = new Thread(t);
//启动线程
t1.start();
t2.start();
t3.start();
t4.start();
}
}
线程安全问题产生的原因:
1. 多个线程在操作共享的数据。
2. 操作共享数据的线程代码有多条。
3. 当一个线程在执行操作共享数据的多条代码过程中,其他线程参与了运算,就会导致线程安全问题的产生。
线程安全问题的解决方案—-同步
线程的同步: 就是将多条操作共享数据的线程代码封装起来,当有线程在执行这些代码的时候,其他线程不可以参与运算。 必须要当前线程把这些代码都执行完毕后,其他线程才可以参与运算。
在java中,用同步代码块就可以解决这个问题。
同步代码块的格式:
synchronized(对象)
{
需要被同步的代码;
}
总结:
- 同步的好处:解决了线程的安全问题。
- 同步的弊端:当线程相当多时,因为每个线程都会去判断同步上的锁,这是很耗费资源的,无形中会降低程序的运行效率。
- 同步的前提:必须有多个线程并使用同一个锁。
示例代码—-同步后的卖票程序
/*
需求:卖票程序
多个卖票窗口
*/
class Ticket implements Runnable
{
private int tick = 100;
Object obj = new Object();
public void run()
{
while(true)
{
synchronized (obj)
{
if(tick>0)
{
//显示余票
System.out.println(Thread.currentThread().getName()+"---"+tick--);
}
}
}
}
}
class TicketDemo
{
public static void main(String[] args)
{
//创建Runnable子类的实例对象
Ticket t = new Ticket();
//创建四个线程,模拟四个卖票窗口
Thread t1 = new Thread(t);
Thread t2 = new Thread(t);
Thread t3 = new Thread(t);
Thread t4 = new Thread(t);
//启动线程
t1.start();
t2.start();
t3.start();
t4.start();
}
}
运行结果:
结果分析:
上图显示安全问题已被解决,原因在于Object对象相当于是一把锁,只有抢到锁的线程,才能进入同步代码块向下执行。
总结:
- 要明确哪些代码是多线程运行代码。
- 要明确共享数据
- 要明确多线程运行代码中哪些语句是操作共享数据的。
—————————-割——————————-割——————————–
5. 线程的安全的单例设计模式–懒汉式
饿汉式不存在安全问题,因为不存在多个线程共同操作数据的情况。
懒汉式存在安全问题,可以使用同步函数解决,也可以使用代码块的方式来解决
示例代码
/*
单例设计模式--懒汉式
实例的延迟加载
*/
class Single
{
private static Single s = null;
private Single(){}
public static Single getInstance()
{
if(s ==null)
{
synchronized(Single.class)
{
if(s == null)
s = new Single();
}
}
return s ;
}
}
同步函数和同步代码块的区别:
- 同步函数的锁是固定的this。
- 同步代码块的锁是任意的对象。
建议使用同步代码块。
—————————-割——————————-割——————————–
6. 线程的安全—-死锁
死锁常见情景之一:同步的嵌套。
class Ticket implements Runnable {
private static int num = 100;
Object obj = new Object();
boolean flag = true;
public void run() {
if (flag) {
while (true) {
synchronized (obj) {
show();
}
}
} else
while (true)
show();
}
public synchronized void show() {
synchronized (obj) {
if (num > 0) {
try {
Thread.sleep(10);
} catch (InterruptedException e) {
e.printStackTrace();
}
System.out.println(Thread.currentThread().getName()
+ "...function..." + num--);
}
}
}
}
class DeadLockDemo {
public static void main(String[] args) {
Ticket t = new Ticket();
Thread t1 = new Thread(t);
Thread t2 = new Thread(t);
t1.start();
try {
Thread.sleep(10);
} catch (InterruptedException e) {
e.printStackTrace();
}
t.flag = false;
t2.start();
}
}
由上图可以看到程序已经被锁死,无法向下执行。
由下面代码可以看到,run方法中的同步代码块需要获取obj对象锁,才能执行代码块中的show方法。而执行show方法则必须获取this对象锁,然后才能执行其中的同步代码块。
当线程t1获取到obj对象锁执行同步代码块,线程t2获取到this对象锁执行show方法。 同步代码块中的show方法因无法获取到this对象锁无法执行,show方法中的同步代码块因无法获取到obj对象锁无法执行,就会产生死锁。
另一个示例代码: 便于我们理解
//让一个类实现Runnable接口
class Test implements Runnable {
private boolean flag;
Test(boolean flag) {
this.flag = flag;
}
//复写run方法
public void run() {
if (flag) {
while (true)
synchronized (MyLock.locka) {
System.out.println(Thread.currentThread().getName()
+ "...if locka...");
synchronized (MyLock.lockb) {
System.out.println(Thread.currentThread().getName()
+ "...if lockb...");
}
}
} else {
while (true)
synchronized (MyLock.lockb) {
System.out.println(Thread.currentThread().getName()
+ "...else lockb...");
synchronized (MyLock.locka) {
System.out.println(Thread.currentThread().getName()
+ "...else locka...");
}
}
}
}
}
//定义两个锁
class MyLock {
public static final Object locka = new Object();
public static final Object lockb = new Object();
}
class DeadLockDemo_1 {
public static void main(String[] args) {
Test a = new Test(true);//创建Runnable子类的实例对象
Test b = new Test(false);
Thread t1 = new Thread(a);//创建线程
Thread t2 = new Thread(b);
t1.start();//启动线程
t2.start();
}
}
运行结果:
程序卡住了,每次的运行结果都不一样.
总结: 我们学习死锁是为了在写程序的时候避免这样的情况