java — 线程(一)

线程与进程

进程:是指一个内存中运行的应用程序,每个进程都有一个独立的内存空间,一个应用程序可以同时运行多个进程;进程也是程序的一次执行过程,是系统运行程序的基本单位;系统运行一个程序即是一个进程从创建、运行到消亡的过程。
线程:是进程中的一个执行单元,负责当前进程中程序的执行,一个进程中至少有一个线程。一个进程中是可以有多个线程的,这个应用程序也可以称之为多线程程序。

进程与线程的区别

进程:有独立的内存空间,进程中的数据存放空间(堆空间和栈空间)是独立的,至少有一个线程。
线程:堆空间是共享的,栈空间是独立的,线程消耗的资源比进程小的多。

  1. 因为一个进程中的多个线程是并发运行的,那么从微观角度看也是有先后顺序的,哪个线程执行完全取决于 CPU 的调度,程序员是干涉不了的。而这也就造成的多线程的随机性。
  2. Java 程序的进程里面至少包含两个线程,主进程也就是 main()方法线程,另外一个是垃圾回收机制线程。每当使用 java 命令执行一个类时,实际上都会启动一个 JVM,每一个 JVM 实际上就是在操作系统中启动了一个线程,java 本身具备了垃圾的收集机制,所以在 Java 运行时至少会启动两个线程。
  3. 由于创建一个线程的开销比创建一个进程的开销小的多,那么我们在开发多任务运行的时候,通常考虑创建多线程,而不是创建多进程。

线程的创建

继承Thread类

Java使用java.lang.Thread类代表线程,所有的线程对象都必须是Thread类或其子类的实例。
Java中通过继承Thread类来创建启动多线程的步骤如下:

  1. 定义一个类继承Thread类
  2. 重写run 方法(线程任务)
  3. 开启线程
    创建子类对象
    调用thread类的start方法
class MyThread extends Thread {
	@Override
	public void run() {
		for (int i = 0; i < 50; i++) {
			System.out.println("sub... "+i);
		}
	}
}
public static void main(String[] args) {
    // 创建自定义的线程对象
    MyThread mt = new MyThread();
    // 开启线程
    mt.start();
	// 一个线程仅可以被启动一次
	// mt.start()
    // 主线程执行
    for (int i = 0; i < 50; i++) {
    	System.out.println("main... "+i);
    }
}

线程名字的设置和获取

  • Thread类的方法String getName()可以获取到线程的名字。

  • Thread类的方法setName(String name)设置线程的名字。

  • 通过Thread类的构造方法Thread(String name)也可以设置线程的名字。

public class Demo {
    public static void main(String[] args) {
        MyThread mt = new MyThread();
        //设置线程名字
        mt.setName("旺财");

        mt.start();
    }
}
class MyThread  extends Thread{
    public void run(){
        System.out.println("线程名字:"+super.getName());
    }
}

线程是有默认名字的,如果不设置,JVM会赋予线程默认名字Thread-0,Thread-1。

获取运行main方法线程的名字

Demo类不是Thread的子类,因此不能使用getName()方法获取。
Thread类定义了静态方法static Thread currentThread()获取到当前正在执行的线程对象。

public static void main(String[] args){
	Thread t = Thread.currentThread();
	System.out.println(t.getName());
}

实现Runnable接口

java.lang.Runnable接口类
步骤如下:

  1. 定义Runnable接口的实现类,并重写该接口的run()方法,该run()方法的方法体同样是该线程的线程执行体。
  2. 创建Runnable实现类的实例,并以此实例作为Thread的target来创建Thread对象,该Thread对象才是真正的线程对象。
  3. 调用线程对象的start()方法来启动线程。
public class Demo {
    public static void main(String[] args) {
        // 创建自定义类对象  线程任务对象
        MyRunnable mr = new MyRunnable();
        // 创建线程对象
        Thread t = new Thread(mr);
        t.start();
        for (int i = 0; i < 20; i++) {
            System.out.println("main " + i);
        }
    }
}
public class MyRunnable implements Runnable{
    public void run() {
        for (int i = 0; i < 20; i++) {
        System.out.println(Thread.currentThread().getName()+" "+i);
        }
    }
}

通过实现Runnable接口,使得该类有了多线程类的特征。run()方法是多线程程序的一个执行目标。所有的多线程代码都在run方法里面。Thread类实际上也是实现了Runnable接口的类。

在启动的多线程的时候,需要先通过Thread类的构造方法Thread(Runnable target) 构造出对象,然后调用Thread对象的start()方法来运行多线程代码。

实际上所有的多线程代码都是通过运行Thread的start()方法来运行的。

匿名内部类方式创建线程

使用线程的内匿名内部类方式,可以方便的实现每个线程执行不同的线程任务操作。

public static void main(String[] args) {
        // 第一种方式 实际上需要的就是Thread类的子类对象
        Thread t = new Thread(){
            @Override
            public void run() {
                System.out.println("方式一 赋值!");
            }
        };
        t.start();

        new Thread() {
            @Override
            public void run() {
                System.out.println("方式一 直接调用!");
            }
        }.start();

        // 第二种方式 实际上需要的就是Runnable接口的实现类对象
        Runnable r = new Runnable() {
            @Override
            public void run() {
                System.out.println("方式二 赋值!");
            }
        };
        new Thread(r).start();

        new Thread(new Runnable() {
            @Override
            public void run() {
                System.out.println("方式二 直接调用!");
            }
        }).start();
    }

Thread和Runnable的区别

如果一个类继承Thread,则不适合资源共享。但是如果实现了Runable接口的话,则很容易的实现资源共享。
总结:

实现Runnable接口比继承Thread类所具有的优势:

  1. 适合多个相同的程序代码的线程去共享同一个资源。
  2. 可以避免java中的单继承的局限性。
  3. 增加程序的健壮性,实现解耦操作,代码可以被多个线程共享,代码和线程独立。

Thread类API

睡眠sleep

public static void sleep(long time) 让当前线程进入到睡眠状态,到毫秒后自动醒来继续执行

public class Test{
  public static void main(String[] args){
    for(int i = 1;i<=5;i++){
      	Thread.sleep(1000);
        System.out.println(i)
    }
  }
}

设置线程优先级

线程的切换是由线程调度控制的,我们无法通过代码来干涉,但是我们通过提高线程的优先级最大程度的改善线程获取时间片的几率

线程的优先级被划分为10级,值分别为1-10,其中1最低,10最高.Thread提供了3个常量来表示:

public static final int MIN_PRIORITY   //1 最低优先级
public static final int NORM_PRIORITY  //5 默认优先级
public static final int MAX_PRIORITY   //10 最大优先级
// 更改线程的优先级
public final void setPriority(int newPriority)
    public static void main(String[] args) {
        Thread t1 = new Thread(new Runnable() {
            @Override
            public void run() {
                for (int i = 0; i < 50; i++) {
                    System.out.println("t1: " + i);
                }
            }
        });

        Thread t2 = new Thread(new Runnable() {
            @Override
            public void run() {
                for (int i = 0; i < 50; i++) {
                    System.out.println("t2: " + i);
                }
            }
        });

        // 设置优先级
        t1.setPriority(Thread.MAX_PRIORITY);
        t2.setPriority(Thread.MIN_PRIORITY);

        t1.start();
        t2.start();
    }

join

让当前线程等待,调用方法的线程进行插队先执行,执行完毕后,在让当前线程执行.对其他线程没有任何影响.

注意 此处的当前线程不是调用方法的线程 而是Thread.currentThread().

public static void main(String[] args) throws InterruptedException {
        Thread t1 = new Thread(new Runnable() {
            @Override
            public void run() {
                for (int i = 0; i < 50; i++) {
                    System.out.println("t1: " + i);
                }
            }
        });

        Thread t2 = new Thread(new Runnable() {
            @Override
            public void run() {
                for (int i = 0; i < 50; i++) {
                    System.out.println("t2: " + i);
                }
            }
        });

        t1.start();
        // t1.join();
        /*  t1在此处插队时
            此时 Thread.currentThread() 为 main 线程, 所以 main 线程等待, t1 线程插队
            由于main 线程等待 t2线程还未开启, 因此t1执行完毕, main和t2抢
         */

        t2.start();

        t1.join();
        /*  t1在此处插队时
            此时 Thread.currentThread() 依然为 main 线程, 所以 main 线程等待, t1 线程插队
            而在这之前 t2 线程已经开启, 因此会出现两种情况:
                1. t2 先执行完 t1 执行完之后 主线程执行
                2. t1 先执行完 t2 和 主线程抢
         */

        for (int i = 0; i < 10; i++) {
            System.out.println(Thread.currentThread().getName() + " " + i);
        }

    }
面试题

三个线程同时开启 保证线程一之后执行线程二 再之后执行线程三

class JoinMethodTest {
    public static void main(String[] args) throws InterruptedException {
        Thread t1 = new Thread(new Runnable() {
            @Override
            public void run() {
                for (int i = 0; i < 50; i++) {
                    System.out.println("t1: " + i);
                }
            }
        });

        Thread t2 = new Thread(new Runnable() {
            @Override
            public void run() {
                try {
                    t1.join();
                } catch (InterruptedException e) {
                    e.printStackTrace();
                }
                for (int i = 0; i < 50; i++) {
                    System.out.println("t2: " + i);
                }
            }
        });

        Thread t3 = new Thread(new Runnable() {
            @Override
            public void run() {
                try {
                    t2.join();
                } catch (InterruptedException e) {
                    e.printStackTrace();
                }
                for (int i = 0; i < 50; i++) {
                    System.out.println("t3: " + i);
                }
            }
        });

        t1.start();
        t2.start();
        t3.start();

    }
}

线程停止

public final void stop()  直接中断线程 此方法已过时 不安全
public boolean isInterrupted() 获取线程是否中断的状态 如果中断返回true 没中断返回false
public void interrupt()  中断线程 此时调用interrupted方法 会返回true

代码演示:

public class Test02 {
    public static void main(String[] args)  {
        Thread t1 = new Thread(new Runnable() {
            @Override
            public void run() {

                for (int i = 0; i <1000000 ; i++) {
                    System.out.println(i);
                    //是否有人要中断线程 如果有返回true 如果没有返回false
                    //让线程中断更为平滑 可以使用代码来控制中断
                    boolean b = Thread.currentThread().isInterrupted();
                    if(b){
                        break;
                    }
                }
            }
        });

        t1.start();

        try {
            Thread.sleep(2000);
//            t1.stop(); //方法已经过时 不安全
            t1.interrupt();
        } catch (InterruptedException e) {
            e.printStackTrace();
        }
    }
}

用户线程与守护线程

java分为两种线程:用户线程和守护线程

/*
    用户线程
        我们正常写的代码都是用户线程
    守护线程
        用户线程存在 守护线程可以执行 用户线程执行完成 守护线程即使没有执行完 jvm也会退出

    守护线程和用户线程没有本质的区别,唯一不同之处就在于虚拟机的退出:
    如果用户线程已经全部退出运行了,只剩下守护线程存在,虚拟机会直接退出.但是只要有用户线程运行,虚拟机就不会退出.

    设置守护线程:
        public final void setDaemon(boolean on) on的值为true将线程设置为守护线程,需要在开启线程之前设置
*/
    public static void main(String[] args) {
        Thread t1 = new Thread(new Runnable() {
            @Override
            public void run() {
                for (int i = 0; i < 50; i++) {
                    try {
                        Thread.sleep(200);
                    } catch (InterruptedException e) {
                        e.printStackTrace();
                    }
                    System.out.println("t1: " + i);
                }
            }
        });
        Thread t2 = new Thread(new Runnable() {
            @Override
            public void run() {
                for (int i = 0; i < 50; i++) {
                    try {
                        Thread.sleep(50);
                    } catch (InterruptedException e) {
                        e.printStackTrace();
                    }
                    System.out.println("t2: " + i);
                }
            }
        });

        /*
            由于t2每次休眠50ms, 而t1休眠200ms, 所以t2先执行完
            而t1并不是守护线程, 所以t2执行完后, t1继续执行
         */
        // t1.start();
        // t2.start();

        //将t1设置为守护线程, t2执行完后, t1将不再执行, jvm直接退出
        t1.setDaemon(true);
        t1.start();
        t2.start();
    }

线程安全

多条线程操作同一资源时 可能出现数据错误 此时线程是不安全的

public class Demo {
    public static void main(String[] args) {
        SellTickets st = new SellTickets();
        Thread t1 = new Thread(st);
        Thread t2 = new Thread(st);
        Thread t3 = new Thread(st);

        t1.start();
        t2.start();
        t3.start();
    }
}
class SellTickets implements Runnable {
    private int i = 20;
    public void run() {
        while (true) {
            if (i > 0){
                try {
                    Thread.sleep(10);
                } catch (InterruptedException e) {
                    e.printStackTrace();
                }
                System.out.println(Thread.currentThread().getName() + " 卖票 " + i--);
            }
        }
    }
}
/*
Thread-2 卖票 19
Thread-0 卖票 20
Thread-1 卖票 18
Thread-1 卖票 17
Thread-2 卖票 15
Thread-0 卖票 16
Thread-2 卖票 14
Thread-0 卖票 13
Thread-1 卖票 12
Thread-1 卖票 10
Thread-0 卖票 9
Thread-2 卖票 11
Thread-2 卖票 8
Thread-0 卖票 8
Thread-1 卖票 6
Thread-0 卖票 5
Thread-2 卖票 4
Thread-1 卖票 3
Thread-2 卖票 2
Thread-1 卖票 1
Thread-0 卖票 0
Thread-2 卖票 -1
*/

发现程序出现了两个问题:

  1. 相同的票数
  2. 不存在的票,比如0票与-1票,是不存在的。

线程同步

当我们使用多个线程访问同一资源的时候,且多个线程中对资源有写的操作,就容易出现线程安全问题。

要解决上述多线程并发访问一个资源的安全性问题:也就是解决重复票与不存在票问题,Java中提供了同步机制(synchronized)来解决。

根据案例简述:

窗口1线程进入操作的时候,窗口2和窗口3线程只能在外等着,窗口1操作结束,窗口1和窗口2和窗口3才有机会进入代码去执行。
也就是说在某个线程修改共享资源的时候,其他线程不能修改该资源,等待修改完毕同步之后,才能去抢夺CPU资源,完成对应的操作,保证了数据的同步性,解决了线程不安全的现象。

为了保证每个线程都能正常执行原子操作,Java引入了线程同步机制。

同步代码块

同步代码块:线程操作的共享数据进行同步。synchronized关键字可以用于方法中的某个区块中,表示只对这个区块的资源实行互斥访问。

// 格式
synchronized(同步锁){
     需要同步操作的代码
}

同步锁:

同步锁又称为对象监视器。同步锁只是一个概念,可以想象为在对象上标记了一个锁.

  1. 锁对象 可以是任意类型。
  2. 多个线程对象 要使用同一把锁。

注意:在任何时候,最多允许一个线程拥有同步锁,谁拿到锁就进入代码块,其他的线程只能在外等着(BLOCKED)。

使用同步代码块解决代码:

public class Ticket implements Runnable{
	private int ticket = 100;
	private Object lock = new Object();
	/*
	 * 执行卖票操作
	 */
	@Override
	public void run() {
		//每个窗口卖票的操作 
		//窗口 永远开启 
		while(true){
			synchronized (lock) {
				if(ticket>0){//有票 可以卖
					//出票操作
					//使用sleep模拟一下出票时间 
					try {
						Thread.sleep(50);
					} catch (InterruptedException e) {
						// TODO Auto-generated catch block
						e.printStackTrace();
					}
					//获取当前线程对象的名字 
					String name = Thread.currentThread().getName();
					System.out.println(name+"正在卖:"+ticket--);
				}
			}
		}
	}
}

注意:线程运行至同步代码块的时候,需要判断锁,获取锁,出去同步代码块后要释放锁,增加了很多操作,因此线程安全,程序的运行速度慢!

同步方法

同步方法:当一个方法中的所有代码,全部是线程操作的共享数据的时候,可以将整个方法进行同步。使用synchronized修饰的方法,就叫做同步方法,保证A线程执行该方法的时候,其他线程只能在方法外等着。

// 格式
public synchronized void method(){
   // 可能会产生线程安全问题的代码
}

使用同步方法代码如下:

public class Ticket implements Runnable{
	private int ticket = 100;
	/*
	 * 执行卖票操作
	 */
	@Override
	public void run() {
		//每个窗口卖票的操作 
		//窗口 永远开启 
		while(true){
			sellTicket();
		}
	}
	
	/*
	 * 锁对象 是 谁调用这个方法 就是谁 
	 *   隐含 锁对象 就是  this
	 *    
	 */
	public synchronized void sellTicket(){
        if(ticket>0){//有票 可以卖	
            //出票操作
            //使用sleep模拟一下出票时间 
            try {
              	Thread.sleep(100);
            } catch (InterruptedException e) {
              	e.printStackTrace();
            }
            //获取当前线程对象的名字 
            String name = Thread.currentThread().getName();
            System.out.println(name+"正在卖:"+ticket--);
        }
	}
}

同步锁是谁?

  • 对于非static方法,同步锁就是this。

  • 对于static方法,我们使用当前方法所在类的字节码对象(类名.class)。

Lock锁

java.util.concurrent.locks.Lock机制提供了比synchronized代码块和synchronized方法更广泛的锁定操作,同步代码块/同步方法具有的功能Lock都有,除此之外更强大,更体现面向对象。

Lock锁也称同步锁,加锁与释放锁方法化了,如下:

  • public void lock() :加锁。
  • public void unlock():释放锁。

使用如下:

public class Ticket implements Runnable{
	private int ticket = 100;
	
	Lock lock = new ReentrantLock();
	/*
	 * 执行卖票操作
	 */
	@Override
	public void run() {
		//每个窗口卖票的操作 
		//窗口 永远开启 
		while(true){
			lock.lock();
			if(ticket>0){//有票 可以卖
				//出票操作 
				//使用sleep模拟一下出票时间 
				try {
					Thread.sleep(50);
				} catch (InterruptedException e) {
					// TODO Auto-generated catch block
					e.printStackTrace();
				}
				//获取当前线程对象的名字 
				String name = Thread.currentThread().getName();
				System.out.println(name+"正在卖:"+ticket--);
			}
			lock.unlock();
		}
	}
}

线程状态

这里先列出各个线程状态发生的条件,下面将会对每种状态进行详细解析。

线程状态 导致状态发生条件
NEW(新建) 线程刚被创建,但是并未启动。还没调用start方法。
Runnable(可运行) 线程可以在java虚拟机中运行的状态,可能正在运行自己代码,也可能没有,这取决于操作系统处理器。
Blocked(锁阻塞) 当一个线程试图获取一个对象锁,而该对象锁被其他的线程持有,则该线程进入Blocked状态;当该线程持有锁时,该线程将变成Runnable状态。
Waiting(无限等待) 一个线程在等待另一个线程执行一个(唤醒)动作时,该线程进入Waiting状态。进入这个状态后是不能自动唤醒的,必须等待另一个线程调用notify或者notifyAll方法才能够唤醒。
Timed Waiting(计时等待) 同waiting状态,有几个方法有超时参数,调用他们将进入Timed Waiting状态。这一状态将一直保持到超时期满或者接收到唤醒通知。带有超时参数的常用方法有Thread.sleep 、Object.wait。
Teminated(被终止) 因为run方法正常退出而死亡,或者因为没有捕获的异常终止了run方法而死亡。

等待和唤醒

Object类的方法

void wait()
// 在其他线程对用此对象的notify()方法或notifyAll方法前, 导致当前线程等待
void wait(long time)
// 当前线程等待 time 毫秒
void notify()
// 唤醒在此对象监视器上等待的单个线程
void notifyAll()
// 唤醒在此对象监视器上等待的所有线程
public class RelatedInObject {
    public static void main(String[] args) throws InterruptedException {
        Object o = new Object();
        new Thread(new Runnable() {
            @Override
            public void run() {
                synchronized (o) {
                    System.out.println("开始等待");
                    try {
                        o.wait();
                         // 等待3000毫秒后
                         // o.wait(3000);
                        System.out.println("已经被唤醒");
                    } catch (InterruptedException e) {
                        e.printStackTrace();
                    }
                }
                System.out.println("end");
            }
        }).start();

        Thread.sleep(5000);

        synchronized (o) {
            System.out.println("开始唤醒");
            o.notify();
        }
    }
}

原文链接:https://www.cnblogs.com/paopaoT/p/17316036.html

本站文章如无特殊说明,均为本站原创,如若转载,请注明出处:java — 线程(一) - Python技术站

(0)
上一篇 2023年4月18日
下一篇 2023年4月18日

相关文章

  • Java8中stream和functional interface的配合使用详解

    下面我将给出一个详细讲解“Java8中stream和functional interface的配合使用”的攻略: 一、什么是stream和functional interface 1.1 stream 在Java中,Stream是Java 8提供的一个新特性。Stream提供了一种更便捷的方式来处理集合数据,可以实现很多操作,比如过滤,排序和统计等。Stre…

    Java 2023年5月26日
    00
  • java实现动态时钟并设置闹钟功能

    Java实现动态时钟并设置闹钟功能 概述 本攻略将介绍如何使用Java语言实现一个动态时钟并设置闹钟功能。该时钟将会不断更新并显示当前的时间,并允许用户设置一个闹钟时间。当时钟时间到达设置的闹钟时间时,用户将会收到一条提示消息。 实现过程 步骤一:创建界面和布局 我们可以使用Swing工具箱来创建用户界面,如下所示: public class Clock e…

    Java 2023年5月20日
    00
  • 在SpringBoot中使用JWT的实现方法

    下面我将为您讲解在SpringBoot中使用JWT的实现方法的完整攻略。 1. 什么是JWT JWT全称是Json Web Token,它是一种基于 JSON 的开放标准(RFC 7519) ,用于在不同的系统之间传递信息,并且保证信息不会被篡改。在进行用户认证、鉴权等领域,JWT被广泛应用。 JWT由三部分组成: Header 头部 Payload 载荷(…

    Java 2023年5月19日
    00
  • 深入了解Java8中的时区日期时间

    关于“深入了解Java8中的时区日期时间”的攻略,我将从以下几个方面进行详细讲解: 时区概念介绍 Java8中的时区 日期时间的表示和操作 时区转换和格式化 时区概念介绍 时区是一个地球上的地区,为方便起见,划分为24个标准时区,每个时区以相对于格林威治标准时间的小时数进行标记。时区与地球上的经度有密切关系,通常是基于一个参考点来描述小时数。例如,北京的时区…

    Java 2023年5月20日
    00
  • Java字符串格式化,{}占位符根据名字替换实例

    Java字符串格式化是一种很常用的字符串处理方式,可以将占位符替换为实际的数据。其中,{}是常见的占位符,可以根据顺序或者名字进行替换。本文将详细讲解使用{}占位符根据名字替换的实现方法和示例。 使用{}占位符根据名字替换的方法 在Java中,可以使用String.format()方法进行字符串格式化,其中{}用来表示占位符,可以通过指定参数顺序或者参数名来…

    Java 2023年5月27日
    00
  • PHP小程序后台部署运行 LNMP+WNMP的方法

    下面是“PHP小程序后台部署运行 LNMP+WNMP的方法”的完整攻略。 概述 在运行PHP小程序时,我们需要将代码部署在服务器上并通过HTTP访问。为了实现这一目的,我们可以使用LNMP或WNMP环境,其中LNMP代表Linux+Nginx+MySQL+PHP,WNMP代表Windows+Nginx+MySQL+PHP。在本攻略中,我们将分别介绍如何在Li…

    Java 2023年5月23日
    00
  • Spring MVC创建项目踩过的bug

    以下是关于“Spring MVC创建项目踩过的bug”的完整攻略,其中包含两个示例。 Spring MVC创建项目踩过的bug 在创建Spring MVC项目时,我们可能会遇到一些常见的问题。在本文中,我们将讲解一些常见的问题及其解决方法。 问题1:404错误 在创建Spring MVC项目时,我们可能会遇到404错误。这通常是由于Spring MVC配置不…

    Java 2023年5月17日
    00
  • Spring boot从安装到交互功能实现零基础全程详解

    Spring boot从安装到交互功能实现零基础全程详解 本文将详细讲解如何从零开始安装和使用Spring Boot,以及如何实现基本的交互功能,让你从零基础到实现一个Spring Boot应用项目。 安装 首先,你需要安装Java和Maven。 安装Java 访问Oracle官网,下载并安装最新版本的JDK。 安装Maven 访问Apache Maven官…

    Java 2023年5月15日
    00
合作推广
合作推广
分享本页
返回顶部