多线程
程序和进程的概念
程序:硬盘上存储的,静止的代码
进程:程序的一次执行的产生进程,每个进程对应一定的内存空间,并且 只能使用自己的内存空间,各个进程之间互不干扰
进程称为操作系统资源分配的基本单位
并发和并行
并发:在一段时间内多个进程轮流使用同一个CPU,多个进程形成并发
并行:在同一时刻多个进程使用各自的CPU,多个进程形成并行,并行需要多个CPU支持
并发是能够让操作系统从宏观看起来同一时间段执行多个任务,操作系统一般通过CPU时间片轮转实现并发
线程
线程的出现为了解决实时性问题
线程:进程中的一个执行任务(控制单元),一个进程中可以运行多个线程,多个线程可共享数据,线程是CPU调度的基本单位
多线程:在同一进程中同时运行的多个程序
主线程main:在运行一个简单的Java程序的时候,就已经存在了两个线程,一个是主线程,一个是后台线程(维护) 的垃圾回收
进程与线程间的区别
- 根本区别:进程是操作系统资源分配的基本单位,而线程是CPU调度和执行的基本单位
- 开销方面:每个进程都有独立的代码和数据空间,程序之间切换会有较大的开销,线程可以看作轻量级的进程,同一类线程共享代码和数据空间,线程之间切换的开销小
- 所处环境:在操作系统中能同时运行多个进程,同一个进程中有多个线程同时执行
- 内存分配方面:进程占用内存空间比线程大,线程除CPU外,系统不会为线程分配内存(线程所使用的资源来自所属进程的资源)
- 包含关系:没有线程的进程可以看成是单线程,线程是进程的一部分
实现线程的方式
- 继承Thread
- 实现Runnable接口
继承Thread
- 自定义继承Thread实现多线程
- 必须重写run方法
- 创建自定义类对象
- 自定义类对象调用start方法
自定义类
public class BThread extends Thread{
@Override
public void run() {
for (int i = 0; i < 10; i++) {
System.out.println("BThread==>"+i);
}
}
}
测试类
public class Test01 {
public static void main(String[] args) {
//创建一个线程对象
BThread bThread = new BThread();
//启动线程
bThread.start();//调用Thread的start方法,JVM会自动调用run方法
//在main线程中执行
for (int i = 0; i < 5; i++) {
System.out.println("main :" + i);
}
}
}
多个线程轮流使用CPU,先抢到的先使用,执行结果不固定
实现Runnable接口
- 自定义类实现Runnable接口
- 重写run方法
- 创建自定义类对象
- 创建Thread类,把自定义对象传进其构造器
- 调用Thread对象的start方法
自定义类
//CThread不是线程,但是具备在线程中执行的能力
public class CThread implements Runnable{
@Override
public void run() {
for (int i = 0; i < 10; i++) {
System.out.println("CThread==>"+i);
}
}
}
测试类
public class Test02 {
public static void main(String[] args) {
//创建CThread对象
CThread c = new CThread();
//创建线程,把c放入线程中执行
Thread thread = new Thread(c);
thread.start();
}
}
总结
- 继承Thread类后,不能继承其他类;实现Runnable接口还可以继承其他类
- 实现Runnable接口更方便共享资源,同一份资源,多个线程并发访问
例子
模拟买票过程
- 使用继承Thread
public class TicketThread extends Thread{
private static int num =5;
public TicketThread(String name) {
super(name);
}
public TicketThread() {
}
@Override
public void run() {
//模拟每个窗口有10个人买票
for (int i = 0; i < 10; i++) {
if (num>0){
num--;
System.out.println(super.getName()+" 剩余"+num+"张票");
}
}
}
}
测试类
public class Test01 {
public static void main(String[] args) {
//模拟买票
TicketThread ta = new TicketThread("窗口A");
TicketThread tb = new TicketThread("窗口B");
TicketThread tc = new TicketThread("窗口C");
TicketThread td = new TicketThread("窗口D");
TicketThread te = new TicketThread("窗口E");
ta.start();
tb.start();
tc.start();
td.start();
te.start();
}
}
- 使用实现
public class TicketThread01 implements Runnable{
private int count=5;
@Override
public void run() {
for (int i = 0; i < 10; i++) {
if (count>0){
count--;
System.out.println(Thread.currentThread().getName()+"卖出了一张票,剩余"+count);
}
}
}
}
测试类
public class Test01 {
public static void main(String[] args) {
TicketThread01 t = new TicketThread01();
Thread thread = new Thread(t,"窗口1");
Thread thread2 = new Thread(t,"窗口2");
Thread thread3 = new Thread(t,"窗口3");
thread.start();
thread2.start();
thread3.start();
}
}
多线程访问共享资源时,存在严重问题,那就是导致访问共享资源数据错误问题
原因
1.线程通过抢占CPU的方式工作,执行过程中,随时可能CPU时间片被挂起,在程序的任何地方可能被随时切换出去
2.由于被随时切换出去或者被挂起,所以导致访问共享资源数据错乱
线程生命周期和状态
- 新建:程序使用new创建一个线程后,该线程处于新建状态
- 可运行状态:Runnable状态可细分为两个状态.
- 就绪状态(Ready):当线程调用start(),该线程处于就绪状态,进入线程队列排队(此时并未开始执行,具体运行取决于CPU调度)
- 运行状态(Running):表示某线程对象被CPU调度器的调度,执行线程体.在运行状态的线程执行自己的 run 方法中代码,直到等待某资源而阻塞或完成任 何而死亡。
- 阻塞状态(blocked):正在运行的线程遇到某个特殊情况如,执行sleep(), 同步、等待I/O操作完成等。 进入阻塞状态的线 程让出CPU资源,并暂时停止自己的执行。
- 死亡状态(terminated):表示线程终止,当线程执行完成或者线程抛出未捕获的异常或者被强制终止
线程的常用方法
-
Join方法:线程的强制执行 t1.join(),t1强制执行,导致其他线程(mainThread)阻塞,当t1执行完毕后,其他线程阻塞原因消除,进入就绪状态
-
线程休眠sleep():线程调用sleep(毫秒)方法,当前线程进入阻塞状态,阻塞时间到后进入就绪状态
- 休眠状态过程可以被中断,所以存在一个检查时异常
InterruptedException
异常,当外界程序中断该线程时,休眠提前结束,进入就绪状态,等待CPU调度
try { Thread.sleep(5000); } catch (InterruptedException e) { e.printStackTrace(); } // 中断线程 t1.interrupt();
- 休眠状态过程可以被中断,所以存在一个检查时异常
-
线程优先级:优先级越高,被CPU调动的可能性越大,但不一定是优先级越高就一定先执行
三种优先级
System.out.println(Thread.MAX_PRIORITY);
System.out.println(Thread.MIN_PRIORITY);
System.out.println(Thread.NORM_PRIORITY);
-
线程礼让yield : 线程礼让后,进入就绪状态,也不一定能够成功礼让,只是给了一种暗示
-
线程结束:强制停止一个线程,风险较大(不建议),建议通过interrupt方法引导(异常处理机制)线程正常结束
线程同步
通过上面买票的例子,我们可以发现线程存在安全性问题,即当多线程并发访问同一个资源对象的时候,可能出现线程不安全的问题。
解决方案
在多线程环境下,如果对共享资源进行破坏性操作时,需要同步操作
同步操作:如果需要一系列操作,要么都执行,要么都不执行,称为原子性操作(也认为业务上的不可分割)
同步代码块
synchronized(同步锁){
//需要同步操作的代码
}
同步锁,也称为同步监听对象/同步监听器/互斥锁
在任何时候,最多允许一个线程拥有同步锁,谁拿到锁就执行,其他的线程只能在代码块外等
着。
例子
要想实现原子性操作,就必须对共享资源加锁,已经加锁的线程,如果CPU时间片到了,会进入就绪状态,其他线程争夺CPU调度,但由于已经加锁,所以会进入到阻塞状态,知道原本加锁的线程运行结束
public class TicketThread01 implements Runnable{
private int count=100;
@Override
public void run() {
for (int i = 0; i < 100; i++) {
//同步锁this表示count对象,程序中count对象只有一份,所以被作为同步锁
synchronized (this){
if (count>0){
try {
Thread.sleep(500);
} catch (InterruptedException e) {
e.printStackTrace();
}
count--;
System.out.println(Thread.currentThread().getName()+"卖出了一张票,剩余"+count);
}
}
}
}
}
判断同步锁是谁?
- 对于非static方法,当前对象this就是同步锁
- 对于static方法,同步锁就是当前方法所在类的字节码对象(除去自己本身的静态空间)类名.class
同步方法
[修饰符] synchronized 返回值类型 方法名称(、、、){
// 原子性操作
}
例子
public class TicketThread01 implements Runnable{
private int count=100;
@Override
public void run() {
for (int i = 0; i < 100; i++) {
}
}
public synchronized void STicket(){
//同步锁this表示count对象,程序中count对象只有一份,所以被作为同步锁
if(count>0){
try {
Thread.sleep(500);
} catch (InterruptedException e) {
e.printStackTrace();
}
count--;
System.out.println(Thread.currentThread().getName()+"卖出了一张票,剩余"+count);
}
}
}
synchronized的优劣
优点:保证了多线程并发访问时的同步操作,避免线程的安全性问题。
缺点:使用synchronized的方法/代码块的性能要低一些
同理,StringBuilder和StringBuffer的区别
就是方法中有没有使用synchronized的区别