多线程
多线程的创建方式
方式1:继承于Thread类
1.创建一个集成于Thread类的子类 (通过ctrl+o(override)输入run查找run方法)
2.重写Thread类的run()方法
3.创建Thread子类的对象
4.通过此对象调用start()方法
start与run方法的区别:
start方法的作用:1.启动当前线程 2.调用当前线程的重写的run方法(在主线程中生成子线程,有两条线程)
调用start方法以后,一条路径代表一个线程,同时执行两线程时,因为时间片的轮换,所以执行过程随机分配,且一个线程对象只能调用一次start方法。
run方法的作用:在主线程中调用以后,直接在主线程一条线程中执行了该线程中run的方法。(调用线程中的run方法,只调用run方法,并不新开线程)
package com.example.paoduantui.Thread;
import android.view.Window;
/**
*
* 创建三个窗口卖票,总票数为100张,使用继承自Thread方式
* 用静态变量保证三个线程的数据独一份
*
* 存在线程的安全问题,有待解决
*
* */
public class ThreadDemo extends Thread{
public static void main(String[] args){
window t1 = new window();
window t2 = new window();
window t3 = new window();
t1.setName("售票口1");
t2.setName("售票口2");
t3.setName("售票口3");
t1.start();
t2.start();
t3.start();
}
}
class window extends Thread{
private static int ticket = 100; //将其加载在类的静态区,所有线程共享该静态变量
@Override
public void run() {
while(true){
if(ticket>0){
// try {
// sleep(100);
// } catch (InterruptedException e) {
// e.printStackTrace();
// }
System.out.println(getName()+"当前售出第"+ticket+"张票");
ticket--;
}else{
break;
}
}
}
}
方式2:实现Runable接口方式
1.创建一个实现了Runable接口的类
2.实现类去实现Runnable中的抽象方法:run()
3.创建实现类的对象
4.将此对象作为参数传递到Thread类中的构造器中,创建Thread类的对象
5.通过Thread类的对象调用start()
package com.example.paoduantui.Thread;
public class ThreadDemo01 {
public static void main(String[] args){
window1 w = new window1();
//虽然有三个线程,但是只有一个窗口类实现的Runnable方法,由于三个线程共用一个window对象,所以自动共用100张票
Thread t1=new Thread(w);
Thread t2=new Thread(w);
Thread t3=new Thread(w);
t1.setName("窗口1");
t2.setName("窗口2");
t3.setName("窗口3");
t1.start();
t2.start();
t3.start();
}
}
class window1 implements Runnable{
private int ticket = 100;
@Override
public void run() {
while(true){
if(ticket>0){
// try {
// sleep(100);
// } catch (InterruptedException e) {
// e.printStackTrace();
// }
System.out.println(Thread.currentThread().getName()+"当前售出第"+ticket+"张票");
ticket--;
}else{
break;
}
}
}
}
比较创建线程的两种方式:
开发中,优先选择实现Runable接口的方式
原因1:实现的方式没有类的单继承性的局限性
2:实现的方式更适合用来处理多个线程有共享数据的情况
联系:Thread也是实现自Runable,两种方式都需要重写run()方法,将线程要执行的逻辑声明在run中
线程的分类:
java中的线程分为两类:1.守护线程(如垃圾回收线程,异常处理线程),2.用户线程(如主线程)
若JVM中都是守护线程,当前JVM将退出。(形象理解,唇亡齿寒)
线程的状态
线程的生命周期:
线程的生命周期阶段 | 说明 |
---|---|
新建 | 当一个Thread类或其子类的对象被声明并创建时,新生的线程对象处于新建状态 |
就绪 | 处于新建状态的线程被start后,将进入线程队列等待CPU时间片,此时它已具备了运行的条件,只是没分配到CPU资源 |
运行 | 当就绪的线程被调度并获得CPU资源时,便进入运行状态,run方法定义了线程的操作和功能 |
阻塞 | 在某种特殊情况下,被人为挂起或执行输入输出操作时,让出CPU并临时终止自己的执行,进入阻塞状态 |
死亡 | 线程完成了它的全部工作或线程被提前强制性地中止或出现异常导致结束 |
线程的安全问题:
什么是线程安全问题呢?
线程安全问题是指,多个线程对同一个共享数据进行操作时,线程没来得及更新共享数据,从而导致另外线程没得到最新的数据,从而产生线程安全问题。
方式一:同步代码块
使用同步监视器(锁)
Synchronized(同步监视器){
//需要被同步的代码
}
说明:
操作共享数据的代码(所有线程共享的数据的操作的代码)(视作卫生间区域(所有人共享的厕所)),即为需要共享的代码(同步代码块,在同步代码块中,相当于是一个单线程,效率低)
共享数据:多个线程共同操作的数据,比如公共厕所就类比共享数据
同步监视器(俗称:锁):任何一个的对象都可以充当锁。(但是为了可读性一般设置英文成lock)当锁住以后只能有一个线程能进去(要求:多个线程必须要共用同一把锁,比如火车上的厕所,同一个标志表示有人)
Runable天生共享锁,而Thread中需要用static对象或者this关键字或者当前类(window。class)来充当唯一锁
方式二:同步方法
使用同步方法,对方法进行synchronized关键字修饰
将同步代码块提取出来成为一个方法,用synchronized关键字修饰此方法。
对于runnable接口实现多线程,只需要将同步方法用synchronized修饰
而对于继承自Thread方式,需要将同步方法用static和synchronized修饰,因为对象不唯一(锁不唯一)
总结:1.同步方法仍然涉及到同步监视器,只是不需要我们显示的声明。
方式三:JDK5.0新增的lock锁方法
package com.example.paoduantui.Thread;
import java.util.concurrent.locks.ReentrantLock;
class Window implements Runnable{
private int ticket = 100;//定义一百张票
//1.实例化锁
private ReentrantLock lock = new ReentrantLock();
@Override
public void run() {
while (true) {
//2.调用锁定方法lock
lock.lock();
if (ticket > 0) {
try {
Thread.sleep(100);
} catch (InterruptedException e) {
e.printStackTrace();
}
System.out.println(Thread.currentThread().getName() + "售出第" + ticket + "张票");
ticket--;
} else {
break;
}
}
}
}
public class LockTest {
public static void main(String[] args){
Window w= new Window();
Thread t1 = new Thread(w);
Thread t2 = new Thread(w);
Thread t3 = new Thread(w);
t1.setName("窗口1");
t2.setName("窗口1");
t3.setName("窗口1");
t1.start();
t2.start();
t3.start();
}
}
总结:Synchronized与lock的异同?
相同:二者都可以解决线程安全问题
不同:synchronized机制在执行完相应的代码逻辑以后,自动的释放同步监视器
lock需要手动的启动同步(lock()),同时结束同步也需要手动的实现(unlock())(同时以为着lock的方式更为灵活)
优先使用顺序:
LOCK->同步代码块->同步方法