一、基本概念
1、程序
一段静态的代码。
2、进程
进程指程序的一次执行过程,或者是正在运行的一个程序。
特点:
-
独立性:进程是系统中独立存在的实体,它可以拥有自己的独立的资源,每一个进程都拥有自己私有的地址空间。在没有经过进程本身允许的情况下,一个用户进程不可以直接访问其他进程的地址空间。
-
动态性:进程与程序的区别在于,程序只是一个静态的指令集合,而进程是一个正在系统中活动的指令集合。在进程中加入了时间的概念,进程具有自己的生命周期和各种不同的状态,这些概念在程序中都是不具备的。
-
并发性:多个进程可以在单个处理器上并发执行,多个进程之间不会互相影响。
3、线程
线程(thread)是操作系统能够进行运算调度的最小单位。它被包含在进程之中,是进程中的实际运作单位。
一个程序运行后至少一个进程,一个进程里包含多个线程。如果一个进程只有一个线程,这种程序被称为单线程。如果一个进程中有多条执行路径被称为多线程程序。
多线程扩展了多进程的概念,使得同一个进程可以同时并发处理多个任务。
进程和线程的关系:
一个操作系统中可以有多个进程,一个进程中可以有多个线程,每个进程有自己独立的一块内存空间,每个线程与进程内的其他线程一起共享分配给该进程的所有资源,每个线程又有自己独立的内存。
多线程的特性 - - - 随机性:
一个操作系统中,同一时刻,只有一个程序在运行。我们感觉这些程序是在同时进行是因为CPU在高效地进行切换。具体是哪个程序在运行主要看CPU的调度,但是CPU的调度我们根本无法控制,CPU指到是谁,谁就可以干活,没指到就等着,具体是谁在干活就产生了随机性。
4、并发/并行
并发:是指同一时刻,多个程序在抢占共享资源,同时抢占CPU来执行程序
并行:是指同一时刻,有多个CPU在干活,但是一个CPU只干一件事
判断到底是并行还是并发:用 CPU的核数 和 应用程序来比
二、线程状态
线程生命周期,总共有五种状态:
1. 新建状态(New):
当线程对象被创建后,就进入了新建状态,如:Thread t = new MyThread();
2. 就绪状态(可运行状态,Runnable):
当调用线程对象的start()
方法 t.start();
,线程就进入就绪状态。处于就绪状态的线程,只是说明此线程已经做好了准备,随时等待被CPU调度,被调度时才开始正式干活,并不是说执行了start(),此线程立即就会执行,但是由于CPU的执行效率非常快,所以根本感受不到。
3. 运行状态(Running):
当CPU开始调度处于就绪状态的线程时,此时线程才得以真正执行,即进入到运行状态。
注意:就绪状态是进入到运行状态的唯一入口,也就是说,线程要想进入运行状态执行,首先必须处于就绪状态中。
4. 阻塞状态(Blocked):
理想状态下,线程干完活就变成终止状态,但处于运行状态中的线程由于某种原因,暂时放弃对CPU的使用权,停止执行,导致没有成功的把任务完成,此时会进入阻塞状态,直到其再次进入到就绪状态,才有机会再被CPU调用进入到运行状态。
根据阻塞产生的原因不同,阻塞状态可以分为三种:
a) 等待阻塞:运行状态中的线程执行
wait()
方法,使本线程进入到等待阻塞状态;b) 同步阻塞:线程在获取
synchronized
同步锁失败(因为锁被其它线程所占用),它会进入同步阻塞状态;c) 其他阻塞:通过调用线程的
sleep()
或join()
或发出了I/O请求时,线程会进入到阻塞状态。当sleep()状态超时、join()等待线程终止或者超时、或者I/O处理完毕时,线程会重新转入就绪状态。
5. 终止状态(Dead):
线程执行完了或者因异常退出了run()
方法,该线程结束生命周期。
注意:sleep()和wait()的区别
- Thread类的方法:sleep(),yield()等 ;
Object的方法:wait(),notify()等 。- 每个对象都有一个锁来控制同步访问。Synchronized关键字可以和对象的锁交互,来实现线程的同步。 sleep方法没有释放锁; wait方法释放了锁,使得其他线程可以使用同步控制块或者方法。
- wait,notify和notifyAll只能在同步控制方法或者同步控制块里面使用,而sleep可以在任何地方使用。
- sleep必须捕获异常,而wait,notify和notifyAll不需要捕获异常。
三、多线程的创建
1、继承Thread类
Thread类本质上是实现了Runnable接口的一个实例,代表一个线程的实例。启动线程的唯一方法就是通过Thread类的start()
实例方法。
start()
方法是一个native方法,它将通知底层操作系统,最终由OS启动一个新线程,它将执行run()方法。这种方式实现多线程很简单,通过自己的类 extend Thread
,并复写run()
方法,就可以启动新线程并执行自己定义的run()方法。
构造方法:
Thread() 分配新的 Thread 对象。
Thread(String name) 分配新的 Thread 对象。
Thread(Runnable target) 分配新的 Thread 对象。
Thread(Runnable target,String name) 分配新的 Thread 对象。
常用方法:
String getName()
返回该线程的名称。
static Thread currentThread()
返回对当前正在执行的线程对象的引用。
void setName(String name)
改变线程名称,使之与参数 name 相同。
static void sleep(long millis)
在指定的毫秒数内让当前正在执行的线程休眠(暂停执行),此操作受到系统计时器和调度程序精度和准确性的影响。
void start()
使该线程开始执行;Java 虚拟机调用该线程的 run 方法。
Thread(String name)
分配新的 Thread 对象。
实现:
public class Test {
public static void main(String[] args) {
//3、创建线程对象
MyThread t1 = new MyThread();//新建状态--默认线程名称
MyThread t2 = new MyThread("joy");//新建状态--自定义线程名称(调含参构造)
//4、启动线程:使该线程开始执行,JVM调用该线程的run()方法(谁抢到资源谁就先执行)
t1.start();//从新建状态变成可运行状态
t2.start();
//6、直接调用run()也可执行业务,但它只是一个普通的实现方式,并不会发生多线程现象,没有多线程编程的特点
//t1.run();
}
}
//1、创建多线程类MyThread继承Thread类
class MyThread extends Thread{
//7、修改线程名称Thread(String name) 自动生成--使用父类构造方法
public MyThread(){
super();
}
public MyThread(String name) {
super(name);
}
//2、把多线程业务的代码放入run()
@Override
public void run() {
//5、默认在子类中调父类的功能
//super.run();
for (int i = 0; i < 10; i++) {
System.out.println(getName()+i);//获取线程名称
}
}
}
2、实现Runnable接口
如果自己的类已经继承了另一个类,就无法多继承,此时可以实现一个Runnable接口。
常用方法:
void run()
使用实现接口 Runnable 的对象创建一个线程时,启动该线程将导致在独立执行的线程中调用对象的 run 方法。
实现:
public class Test {
public static void main(String[] args) {
//3、创建多个线程对象执行任务
//--接口里没有构造方法,不能直接new,需要把接口类型的对象转成Thread类型,然后调用Thread的start()启动线程,用构造方法--Thread(Runnable target)
MyRunnable my = new MyRunnable();
Thread t1 = new Thread(my);//默认线程名称
Thread t2 = new Thread(my,"rose");//自定义线程名称
//4、启动线程:使该线程开始执行,JVM调用该线程的run()方法(谁抢到资源谁就先执行)
t1.start();
t2.start();
}
}
//1、创建实现类实现Runnable接口
class MyRunnable implements Runnable{
//2、把多线程业务的代码放入run()
@Override
public void run() {
for (int i = 0; i < 10; i++) {
System.out.println(Thread.currentThread().getName()+"---"+i);//获取当前正在执行任务的线程对象
}
}
}
3、多线程创建方式的比较
四、多线程售票案例
需求:总共100张票,开设4个窗口卖票。
1、多线程并发时的数据安全问题
方案1:继承Thread
//这个类用来测试多线程售票案例
public class Test_Tickets {
public static void main(String[] args) {
//3、创建多线程测试
MyTicket target = new MyTicket();
target.start();//启动线程,底层会自动调用run(),执行业务
MyTicket target2 = new MyTicket();
target2.start();
MyTicket target3 = new MyTicket();
target3.start();
MyTicket target4 = new MyTicket();
target4.start();
}
}
//1、创建线程类
class MyTicket extends Thread{
//问题1:重卖--卖了400张票?因为tickets是成员变量,对每个对象都可见,都可以进行操作,自己存了自己的100
// int tickets = 100 ; //定义变量,记录票的总数---因为这个是共享数据,一个对象一份,四个对象就是四份一共卖了400张票
static int tickets = 100 ; //定义变量,记录票的总数---加static变成共享数据
@Override
public void run() {
//2、写业务,一直在卖票
while(true) {//死循环
if(tickets>0) {
try {
//问题2:超卖--让程序睡10ms后,卖出了0,-1,-2张票??待解决!!!
//问题3:重复卖--把一张票卖给了多个人?待解决!!!
Thread.sleep(10);//延迟访问,让程序睡一会来检测多线程编程中的数据安全问题,数据没问题才证明程序完美
} catch (InterruptedException e) {
e.printStackTrace();
}
System.out.println(getName()+"=="+tickets--);
}
if(tickets<=0) {
break;//设置出口!!!
}
}
}
}
方案2:实现Runnable
public class Test1_MyTicket {
public static void main(String[] args) {
// 启动线程,自动执行run()里的业务
MyTicket target = new MyTicket();
Thread t1 = new Thread(target, "1号窗口:");
t1.start();
Thread t2 = new Thread(target, "2号窗口:");
t2.start();
Thread t3 = new Thread(target, "3号窗口:");
t3.start();
Thread t4 = new Thread(target, "4号窗口:");
t4.start();
}
}
// 创建多线程类
// 问题1重复卖:同一张票卖给了多个人
// 问题2超卖:产生了0 -1 -2 这样的票
class MyTicket implements Runnable {
int tikcets = 100; // 定义变量,保存票的数量
@Override
public void run() {
// 一直卖票
while (true) {
//当 tikcets = 100时,t1 t2 t3 t4都满足条件,4个人一起开始卖票
//当 tikcets = 1时,t1 t2 t3 t4都满足条件,4个人一起开始卖票
if (tikcets > 0) {
try {
//t1 t2 t3 t4都睡着了
Thread.sleep(10);// 睡10ms
} catch (InterruptedException e) {
e.printStackTrace();
}
//t1醒了,执行了 tikcets--,输出1,tikcets自减变成0
//t2醒了,执行了 tikcets--,输出0,tikcets自减变成-1
//t3醒了,执行了 tikcets--,输出-1,tikcets自减变成-2
//t4醒了,执行了 tikcets--,输出-2,tikcets自减变成-3
// Thread.currentThread().getName()获取当前正在执行任务的线程名称
System.out.println(Thread.currentThread().getName() + tikcets--);
}
if (tikcets <= 0) {
break;
}
}
}
}
出现问题的原因:在多线程程序中,共享数据被两条以上的语句操作,就会造成共享数据错误,出现线程安全问题。
2、解决方案
2.1 概念
异步:多个线程同时对共享资源进行操作,在操作数据时,互相之间不需要等待。提高执行效率,降低了资源的安全性。
同步:多个线程在操作共享资源时,同一时刻只能有一个线程在操作,相当于独占资源,另外的线程必须等待。牺牲了程序的执行效率,提高了资源的安全性。
2.2 同步锁
把那些有多线程并发数据安全隐患的代码用同步包起来,同一时刻只让一个线程执行,其他线程处于等待状态,虽然降低了程序的执行效率和性能,但是提高了共享资源的安全性。
当多个对象操作共享数据时,可以使用同步锁解决线程安全问题。通过synchronized
关键字实现同步。
synchronized(对象){
//需要同步的代码
}
a.锁的对象:
多个线程间要使用同一把锁,才可以保证数据的安全。不能每个线程使用自己的锁,那样并没有真正的锁住共享资源,仍然有数据安全隐患。
b.锁可以存在的位置
要根据业务决定要找到合适的位置。如果锁的范围太大会造成资源浪费,因为锁起来的部分执行效率非常低;如果锁的范围太小,可能根本就没有解决安全问题,数据还是存在隐患。
同步锁synchronized
可以修饰代码块,也可以修饰方法。如果方法里都是同步代码,可以把synchronized
直接写在方法上。
3、解决多线程并发时的数据安全问题
//这个类用来解决多线程并发时,数据安全问题
public class Test_Synchronized {
public static void main(String[] args) {
// 启动线程
MySynchro target = new MySynchro();//只创建一次,就100张票
Thread t1 = new Thread(target, "1号窗口:");
Thread t2 = new Thread(target, "2号窗口:");
Thread t3 = new Thread(target, "3号窗口:");
Thread t4 = new Thread(target, "4号窗口:");
t1.start();
t2.start();
t3.start();
t4.start();
}
}
// 卖100张票
class MySynchro implements Runnable {
//共享资源是静态的,锁字节码文件;共享资源是普通的,使用锁对象this
//static int tickets = 100;
int tickets = 100;
//synchronized用加在方法上,方法里的代码均被同步,默认锁对象this
@Override
synchronized public void run() {
while (true) {
// synchronized修饰代码块:把有问题的代码包起来,实现同步访问
// synchronized (new Object()) {// new Object()是每次产生新的对象,锁了不同对象
// synchronized (obj) {//锁了同一个对象
// synchronized (Ticket2.class) {//锁了本类,针对于静态
// synchronized (this) {// this是多个线程间共享同一个对象(本类对象)
if (tickets > 0) {
try {
Thread.sleep(10);// 暴露数据安全隐患
} catch (InterruptedException e) {
e.printStackTrace();
}
System.out.println(Thread.currentThread().getName() + tickets--);
}
if (tickets <= 0) {
break;
}
// }
}
}
}