多线程
基本概念:程序、进程、线程
- 程序:是为了完成特定任务,用某种语言编写的椅子指令的集合。即指一段静态的代码,静态对象。
- 进程:是程序的依次执行过程,或是正在运行的一个程序。是一个动态的过程:有它自身的产生、存在和消亡的过程。——生命周期
程序是静态的,进程是动态的.
进程作为资源分配的单位,系统在运行时会为每一个今晨分配不同的内存区域。 - 线程:进程可进一步细化为线程,或是正在运行的一个程序。是一个程序内部的一条执行路径。
- 若一个进程同一时间并行执行多个线程,就是支持多线程的。
- 线程作为调度和执行的单位,每个线程拥有独立的运行栈和计算器,线程切换的开销小。
- 一个进程的多个线程共享相同的内存单元/内存地址空间->它们从同一堆中分配对象,可以访问相同的变量和对象。这就使得线程间通信更简便、高效。但多个线程操作共享的系统资源就可能会带来安全的隐患。
- 线程的五种状态:新建状态、就绪状态、运行状态、阻塞状态、死亡状态。(创建、就绪、运行、阻塞、终止。)
单核CPU和多核CPU
-
单核CPU是一种假的多线程,一个时间单元内,只能执行一个线程的任务。(对于单核的CPU来说,在某一个时间点上实际上只能处理一件事情,但是由于CPU的处理速度极快,多个线程之间频繁切换执行,给别人的感觉是:多个事情同时在做!!!)
-
一个Java应用程序,其实至少有三个线程:main()主线程、gc()垃圾回收线程、异常处理线程。如果发生异常,会影响主线程。
-
并行:多个CPU同时执行多个任务。
-
并发:一个CPU(采用事件片)同时执行多个任务。
Java实现线程的两种方式
第一种方式
1. 用new继承线程的类的方式来创建现成的对象。
2. 调用线程对象的start()方法来启动线程。
// 定义线程类
public class MyThread extends Thread{
public void run(){
}
}
// 创建线程对象
MyThread t = new MyThread();
// 启动线程。
t.start();
cuurentThread();//静态方法,返回执行当前代码的线程。
getName();//获取当前线程的名字
setName();//设置当前线程的名字
yield();//释放当前cpu的执行权
注意
- t.run() 不会启动线程,只是普通的调用方法而已。不会分配新的分支栈。(这种方式就是单线程。)
- t.start() 方法的作用是:启动一个分支线程,在JVM中开辟一个新的栈空间,这段代码任务完成之后,瞬间就结束了。
这段代码的任务只是为了开启一个新的栈空间,只要新的栈空间开出来,start()方法就结束了。线程就启动成功了。
启动成功的线程会自动调用run方法,并且run方法在分支栈的栈底部(压栈)。
run方法在分支栈的栈底部,main方法在主栈的栈底部。run和main是平级的。
第二种方式
- 编写一个类,实现java.lang.Runnable接口,实现run方法。
// 定义一个可运行的类
public class MyRunnable implements Runnable {
public void run(){
}
}
// 创建线程对象
Thread t = new Thread(new MyRunnable());
// 启动线程
t.start();
- 继承Thread类的方式不能实现资源的共享,而实现Runnable接口可以实现资源的共享。
//继承Thread类不能实现资源的共享。
public class MyThread extends Thread {
private int ticket = 5;
@Override
public void run() {
super.run();
for (int i = 0; i < 100; i++) {
if(ticket > 0){
System.out.println("卖票:ticket = " + ticket--);
}
}
}
}
class ThreadDemo04 {
public static void main(String[] args) {
MyThread myThread = new MyThread();
MyThread myThread1 = new MyThread();
MyThread myThread2 = new MyThread();
myThread.start();
myThread1.start();
myThread2.start();
}
}
//实现Runnable接口的可以资源共享
public class MyRunnable implements Runnable {
private int ticket = 5;//ticket属性被所有线程所共享
@Override
public void run() {
for(int i = 0; i < 10; i++) {
if(ticket > 0) {
System.out.println("卖票:ticket = " + ticket--);
}
}
}
}
class RunnableTest {
public static void main(String[] args) {
MyRunnable test = new MyRunnable();
new Thread(test).start();
new Thread(test).start();
new Thread(test).start();
}
}
判断线程是否启动
- Java可以使用isAlive()方法来测试线程是否已经启动而且仍然在执行。
class MyThreadAlive implements Runnable{
@Override
public void run() {
for (int i = 0; i < 10; i++) {
System.out.println(Thread.currentThread().getName() +
"运行 --> " + i);
}
}
}
public class ThreadAlive {
public static void main(String[] args) {
MyThreadAlive myAlive = new MyThreadAlive();
Thread thread = new Thread(myAlive,"线程");
System.out.println("线程开始执行之前 --> " + thread.isAlive());
thread.start();
System.out.println("线程开始执行之后 --> " + thread.isAlive());
for (int i = 0; i < 10; i++) {
System.out.println("main 运行 --> " + i);
}
System.out.println("代码执行之后 --> " + thread.isAlive());
}
}
比较创建线程的两种方式
- 开发中:优先选择:实现Runnable接口方式
- 原因:
- 实现的方式没有类的单继承的局限性。
- 实现的方式更适合来处理多个线程有共享数据的情况。
- 联系:public class Thread implements Runnable
- 相同点:两种方式都需要重写run(),将线程要执行的逻辑声明在run()中。
线程调度关系
- 线程名称的格式是:Thread - Xx。
- Java运行时至少会启动两个线程,一个是main线程、另一个是垃圾收集线程。
- 因为线程操作的不确定性,所以主线程有可能最先执行完,此时其他线程不会受到影响,并不会随着主线程的结束而结束。
static Thread currentThread(); //获取当前线程对象
//例:
Thread currentThread = Thread.currentThread();
String getName(); //获取线程对象名字
//例:
System.out.println(currentThread.getName() + "-->" + i);
void setName(String name); //修改线程对象名字
yiled方法(线程的礼让)
- yield()方法不是阻塞方法。让当前线程让位,让给其它线程使用。
- yield()方法的执行会让当前线程从“运行状态”回到“就绪状态”。
- 注意:在回到就绪之后,有可能还会再次抢到。
static void yield(); //让位方法,当前线程暂停,回到就绪状态,让给其它线程。
- 例子:
class MyYield implements Runnable {
@Override
public void run() {
for(int i = 0; i < 5; i++) {
System.out.println(Thread.currentThread().getName()
+ "运行 --> " + i);
if(i == 3) {
System.out.println("线程礼让:");
Thread.yield();
}
}
}
}
public class ThreadYield {
public static void main(String[] args) {
MyYield yield = new MyYield();
Thread thread = new Thread(yield,"线程A");
Thread thread1 = new Thread(yield,"线程B");
thread.start();
thread1.start();
}
}
jion方法(线程的强制运行)
- 在线程操作中,可以使用join方法让一个线程强制运行,线程强制运行期间,其他线程无法运行,必须等到此线程玩长城之后才可以继续运行。
void join();
//例:
t.join(); // 当前线程进入阻塞,t线程执行,直到t线程结束。当前线程才可以继续。
void join(long millis); //接上条,等待该线程终止的时间最长为 millis 毫秒
void join(long millis, int nanos); //接第一条,等待该线程终止的时间最长为 millis 毫秒 + nanos 纳秒
一个线程.join(),当前线程会进入”阻塞状态“。等待加入线程执行完!
sleep方法和interrupt方法(线程的休眠)
- 静态方法:Thread.sleep(1000);参数是毫秒
- 作用: 让当前线程进入休眠,进入“阻塞状态”,放弃占有CPU时间片,让给其它线程使用。
- 这行代码出现在A线程中,A线程就会进入休眠。
- 这行代码出现在B线程中,B线程就会进入休眠。
- Thread.sleep()方法,可以做到这种效果:
间隔特定的时间,去执行一段特定的代码,每隔多久执行一次。 - 休眠一旦中断之后将执行catch中的代码。
static void sleep(long millis); //让当前线程休眠millis秒(毫秒)。
//例:
// 每执行一次该线程睡眠1秒
try {
Thread.sleep(1000);
} catch (InterruptedException e) {
e.printStackTrace();
}
void interrupt(); //终止线程的睡眠
//例:
// 终断t线程的睡眠(这种终断睡眠的方式依靠了java的异常处理机制。)
t.interrupt();
// 5秒之后强行终止t线程
t.stop(); // 已过时(不建议使用。)
//缺点:这种方式存在很大的缺点:容易丢失数据
//因为这种方式是直接将线程杀死了,线程没有保存的数据将会丢失。不建议使用。
//例子:
class MyInterrupt implements Runnable {
@Override
public void run() {
System.out.println("1.进入run方法。");
try {
Thread.sleep(10000);
System.out.println("2.已完成休眠。");
}catch (Exception e) {
System.out.println("3.休眠被终止。");
return;
}
System.out.println("4.线程无法正常结束。");
}
}
public class InterruptTest {
public static void main(String[] args) {
MyInterrupt test = new MyInterrupt();
Thread t = new Thread(test);
t.start();
try {
Thread.sleep(2000);
}catch (Exception e){
}
t.interrupt(); //中断线程的执行
}
}
setDeamon方法(后台线程)
- 只要前台有一个线程在运行,则整个Java进程都不会消失,所以可以设置一个后台线程,这样即使Java进程结束了,此后台线程依然会继续执行,即使用setDaemon()方法。
class MyDaemon implements Runnable {
@Override
public void run() {
while (true) {//死循环
System.out.println(Thread.currentThread().getName()
+ "在运行。");
}
}
}
public class ThreadDaemon {
public static void main(String[] args) {
MyDaemon daemon = new MyDaemon();
Thread thread = new Thread(daemon);
thread.setDaemon(true);
thread.start();
}
}
尽管run()方法中是死循环的方式,但是程序依然可以
执行完,因为方法中的线程操作已经设置成后台运行了。
线程的优先级
- main线程的默认优先级是:5
- 最低优先级1
- 默认优先级是5
- 最高优先级10
//常量
static int MAX_PRIORITY; //最高优先级(10)
static int MIN_PRIORITY; //最低优先级(1)
static int NORM_PRIORITY; //默认优先级(5)
int getPriority(); //获得线程优先级
//例:
System.out.println(hread.currentThread().getName() + "线程的默认优先级是:" + currentThread.getPriority());
void setPriority(int newPriority); //设置线程优先级
//例:
t.setPriority(10);
- 例题:
//理解:线程t1、t2、t3同时去获取资源在i = 0这个资源时,优先级高的先运行,并且t1、t2、t3同时共享资源。到t2时也按优先级共享资源。
class MyPriority implements Runnable {
@Override
public void run() {
for (int i = 0; i < 5; i++) {
try {
Thread.sleep(500);
}catch (Exception e) {
}
System.out.println(Thread.currentThread().getName()
+ "运行,i = " + i);
}
}
}
public class ThreadPriority {
public static void main(String[] args) {
MyPriority priority = new MyPriority();
Thread t1 = new Thread(priority,"线程A");
Thread t2 = new Thread(priority,"线程B");
Thread t3 = new Thread(priority,"线程C");
t1.setPriority(Thread.MIN_PRIORITY);
t2.setPriority(Thread.MAX_PRIORITY);
t3.setPriority(Thread.NORM_PRIORITY);
t1.start();
t2.start();
t3.start();
}
}
线程安全问题
- 线程重点:我们编写的程序需要放到一个多线程的环境下运行,我们更需要关注的式这些数据在多线程并发的环境下是否安全的。
- 数据在多线程环境下存在的安全问题的条件
- 多线程并发。
- 有共享数据。
- 共享数据并有修改行为。
- 解决方法:
- 当多线程并发的环境下,有共享数据,并且这个数据还会被修改,此时就存在线程的安全问题,这个问题的解决方法如下:
线程排队执行。(不能并发)。用排队执行解决线程的安全问题。这种机制被成为:**线程同步机制。**专业术语叫做:线程同步,实际上就是线程不能并发了,必须排队执行。
线程排队会牺牲一部分效率,数据安全是第一位,只有数据安全了,我们才可以谈效率。
- 当多线程并发的环境下,有共享数据,并且这个数据还会被修改,此时就存在线程的安全问题,这个问题的解决方法如下:
同步与死锁
- 一个多线程如果是通过Runnable接口实现的 ,则意味着类中的属性将被多个线程共享,就可能出现资源同步的问题。
同步(同步就是排队)
-
所谓同步,就是指多个操作在同一个时间段内只能有一个线程执行,其他线程要在等待此线程执行完了之后才可以继续执行。
-
同步可以使用同步代码块和同步方法两种方式完成
-
同步代码块:
- 在使用同步代码块时必须指定一个需要同步的对象,但一般都将当前对象设置成同步对象。
synchronized(同步对象){
//需要同步的代码
}
- 例子:
class MySync implements Runnable {
private int ticket = 5;
@Override
public void run() {
for(int i = 0; i < 100; i++) {
synchronized (this) {
if(ticket > 0) {
try {
Thread.sleep(300);
}catch (InterruptedException e) {
e.printStackTrace();
}
System.out.println("卖票:ticket = " + ticket--);
}
}
}
}
}
public class Sync {
public static void main(String[] args) {
MySync sync = new MySync();
Thread thread1 = new Thread(sync);
Thread thread2 = new Thread(sync);
Thread thread3 = new Thread(sync);
thread1.start();
thread2.start();
thread3.start();
}
}
- 同步方法:
- 格式:
synchronized 方法返回值 方法名称(参数列表){
}
- 例子:
class MySync1 implements Runnable {
public int ticket = 5;
@Override
public void run() {
for(int i = 0; i < 100; i++) {
this.sale();
}
}
public synchronized void sale() {
if(ticket > 0) {
try {
Thread.sleep(300);
}catch (InterruptedException e) {
e.printStackTrace();
}
System.out.println("卖票:ticket = " + ticket);
}
}
}
public class Sync1 {
public static void main(String[] args) {
MySync sync = new MySync();
Thread thread1 = new Thread(sync);
Thread thread2 = new Thread(sync);
Thread thread3 = new Thread(sync);
thread1.start();
thread2.start();
thread3.start();
}
}
- 使用这两种方式进行代码块取值和修改值得操作进行了同步,因此不会再出现卖出票为负数的情况。
死锁
-
所谓死锁就是指两个线程都在等待对方先完成,造成了程序的停滞,一般的死锁都是在程序运行中出现的。
-
例子:
package ever;
class Zhangsan {
public void say() {
System.out.println("张三对李四说:\"你给我画,我就把书给你\"。");
}
public void get() {
System.out.println("张三得到画了。");
}
}
class Lisi {
public void say() {
System.out.println("李四对张三说:\"你给我书,我就把画给你\"。");
}
public void get() {
System.out.println("李四得到画了。");
}
}
public class ThreadDeadLock implements Runnable {
private static Zhangsan zs = new Zhangsan();
private static Lisi ls = new Lisi();
private boolean flag = false;
@Override
public void run() {
if(flag) {
synchronized (zs){
zs.say();
try {
Thread.sleep(500);
}catch (InterruptedException e) {
e.printStackTrace();
}
synchronized (ls){
zs.get();
}
}
} else {
synchronized (ls) {
ls.say();
try {
Thread.sleep(500);
} catch (InterruptedException e) {
e.printStackTrace();
}
synchronized (zs) {
ls.get();
}
}
}
}
public static void main(String[] args) {
ThreadDeadLock lock1 = new ThreadDeadLock();
ThreadDeadLock lock2 = new ThreadDeadLock();
lock1.flag = true;
lock2.flag = false;
Thread thread1 = new Thread(lock1);
Thread thread2 = new Thread(lock2);
thread1.start();
thread2.start();
}
}
Object类对线程的支持——等待与唤醒
- public final void wait() throws InterruptedException —>等待线程
- public final void wait(long timeout) throws InterruptedException——>线程等待,并指定等待的最长时间,以毫秒为单位。
- public final void wait(long timeout,int nanos) throws InterruptedException——>线程等待,并指定等待的最长毫秒或纳秒。
- public final void notify()——>唤醒第一个等待的线程。
- public final void notifyAll()——>唤醒全部等待的线程。
线程的生命周期
- 三个新方法:
- suspend()方法:暂时挂起线程
- resume()方法:恢复挂起的线程
- stop()方法:停止线程
- 不推荐使用上三种方法,因为这三种方法在操作时会出现死锁问题。
- 在Java多线程的开发中可以通过设置标志位的方式停止一个线程的运行。