1.线程的概述
1.进程的定义:一个正在执行中的程序 每一个进程执行都有一个执行顺序。该顺序是一个执行路径,或者叫一个控制单元。
2.线程的定义:进程中的一个独立的控制单元,线程控制着进程的执行。
3.进程中至少有一个线程(且java中该线程的代码存在于main方法中,因此称其为主线程)
4.使用多线程的意义:
-实现同时运行的目的
-提高下载效率[如下载工具某雷]
5.一个新的线程就是一个新的执行路径,CPU快速不停地在各个线程间进行切换(表现为各线程抢夺执行权),以达到看似同时运行的效果
多线程的一个特性:随机性
2.创建线程
1.继承Thread类:
步骤:
-定义一个类继承Thread
类
-复写Thread
类中的run
方法 ,run
中放置需要在线程中运行的代码
-实例化该类,调用start
方法[启动线程,调用run方法]
说明:若直接调用run
方法,则没有启动线程。程序依旧是单线程运行
说明:在继承Thread
的子类构造方法中注意super()
的使用
示例代码:
//自定义线程类
class MyThread extends Thread{
public MyThread(String name){
super(name);//直接使用
}
//复写run方法
public void run(){
for(int i=0;i<50;i++){
System.out.println(this.getName()+" run"+i);
}
}
}
public class ThreadManager {
public static void main(String[] args) {
MyThread thread=new MyThread("新的线程");//新建线程
thread.start();//启动线程
}
}
2.实现Runnable接口:
步骤:
- 定义类实现Runnable
接口[implements
关键字]
- 复写Runnable
接口中的run
方法
- 通过Thread
类创建新的线程(将实现Runnable
接口的子类对象作为实参传递给Thread
的构造方法)
- 调用Thread
中的start
方法开启线程并调用Runnable
接口子类中的run
方法
编程思想:如果程序员只需要重写thread
中的run
方法,而不重写其他thread
方法,应该实现runnable
接口。因为除非程序员打算修改或增强类的基本行为,否则不应该为该类创建对象。
示例代码:
//实现Runnable的线程类
class MyNewThread implements Runnable{
public void run() {
System.out.println("线程执行...");
}
}
public class ThreadManagerByIm {
public static void main(String[] args) {
MyNewThread thread=new MyNewThread();
Thread newThread=new Thread(thread);//以Runnable的实现类为实参实例化Thread
newThread.start();//启动线程
}
}
3.实现Runnable方式与继承Thread方式创建线程的区别
- 实现Runnable方式可以避免单继承的局限性[!important]
- 实现Runnable方式使得对象中的资源可以被共享
- 两种方式下线程代码的存放的位置有所不同
3.线程的状态
1.线程对象及名称
Thread currentThread()
返回对当前正在执行的线程对象的引用String getName()
返回该线程的名称void setName(String name)
改变线程名称,使之与参数 name 相同
2.线程状态一览
线程状态 | 说明 |
---|---|
新建 | 线程新建 |
就绪 | 线程运行start 方法 |
运行 | 线程获得CPU资源,即获得CPU执行权 |
阻塞 | 线程处于等待或休眠状态,线程此时具有具备运行资格但没有执行权 |
死亡 | 线程彻底结束 |
4.线程安全性问题
1.实例引入
//售票窗口类
class TicSale implements Runnable{
private int ticket=100;//票数
@Override
public void run() {
while(true){
if(ticket>0){
try{
//令线程短暂休眠
Thread.sleep(10);
}catch(Exception e){} System.out.println(Thread.currentThread().getName()+"当前售出票号:"+ticket--);
}
}
}
}
public class SaleTic {
public static void main(String[] args) {
TicSale ts=new TicSale();
//用两个线程模拟双窗口售票
Thread t1=new Thread(ts);
Thread t2=new Thread(ts);
t1.start();
t2.start();
}
}
运行结果:
…
…
Thread-0当前售出票号:2
Thread-1当前售出票号:2
Thread-0当前售出票号:1
Thread-1当前售出票号:1
…
…
在本例中,由于令售票线程短暂休眠,导致最终售票的结果出现异常情况。由此,可以体会到当多线程对共享数据进行操作时,存在一定的安全性问题。
2.出现安全性问题的原因
当多条语句在操作同一个共享数据时,一个线程对多条语句只执行了一部分(如中途进入休眠状态),另一个线程参与进来执行,就将导致共享数据的错误。
一个可行的解决方案:对多条操作共享数据的语句,只有当一个线程执行完,其他线程才能执行
3.多线程同步代码块
格式:
synchronized(被操作的共享资源所属的对象){
//需要被同步的代码(操作共享数据的语句)
}
说明:需要被同步的对象一般可称为同步锁,一般为this
或是被操作的共享资源所属的对象
(理论上任何对象都可以)。持有锁的线程可以在同步中执行,没有持有锁的线程即使获取了CPU的执行权,也无法进入同步代码块进行执行。
示例代码段:
public void run() {
while(true){
synchronized(this){
if(ticket>0){
try{
//令线程短暂休眠
Thread.sleep(10);
}catch(Exception e){}
System.out.println(Thread.currentThread().getName()+"当前售出票号:"+ticket--);
}
}
}
}
4.多线程同步方法
格式:在普通方法前面添加synchronized关键字(此时同步锁是this)
示例代码段:
public synchronized void sale(){
if(ticket>0){
try{
//令线程短暂休眠
Thread.sleep(10);
}catch(Exception e){}
System.out.println(Thread.currentThread().getName()+"当前售出票号:"+ticket--);
}
}
5.进行同步的前提及弊端
前提:
- 程序必须要有两个或两个以上的线程
- 必须是多个线程使用同一个锁(尤其体现在同步方法与同步代码块中,因此将同步代码块的锁改为
this
)
弊端:
- 多个线程需要判断锁,会消耗更多系统资源
6.分析多线程安全问题的方法:
- 明确哪些代码是多线程运行代码(
run
中的代码及包含的方法) - 明确哪些是共享数据
- 明确多线程运行代码中哪些语句是操作共享数据的
7.静态同步方法
说明:
如果同步方法被静态修饰后,使用的锁是该方法所在类的字节码文件对象[类名.class
获取]
分析:
静态方法进入内存时,内存中没有本类的实例对象。但是有该类对应的字节码文件对象。
通过类名.class
获取 ,该对象的类型是Class