Java多线程快速入门
本篇主要粗略地写了Java多线程的简单使用,由于篇幅有限,故没能做出更深入的介绍,和源码的解释,之后更多更详细的介绍会在之后更新在下面的文章中:
Java 多线程与并发编程(持续更新):https://blog.csdn.net/weixin_39778570/article/details/94998437
概要
Java 给多线程编程提供了内置的支持。 一条线程指的是进程中一个单一顺序的控制流,一个进程中可以并发多个线程,每条线程并行执行不同的任务。
多线程是多任务的一种特别的形式,但多线程使用了更小的资源开销。
这里定义和线程相关的另一个术语 - 进程:一个进程包括由操作系统分配的内存空间,包含一个或多个线程。一个线程不能独立的存在,它必须是进程的一部分。一个进程一直运行,直到所有的非守护线程都结束运行后才能结束。
多线程能满足程序员编写高效率的程序来达到充分利用 CPU 的目的。
线程的生命周期
线程是一个动态执行的过程,它也有一个从产生到死亡的过程。
-
新建状态(New):当线程对象对创建后,即进入了新建状态,如:Thread t = new MyThread();
下图显示了一个线程完整的生命周期。在一个线程调用start方法之前,他就是new状态. -
就绪状态(Runnable):当调用线程对象的start()方法(t.start();),线程即进入就绪状态。处于就绪状态的线程,只是说明此线程已经做好了准备,随时等待CPU调度执行,并不是说执行了t.start()此线程立即就会执行;
-
运行状态(Running):当CPU开始调度处于就绪状态的线程时,此时线程才得以真正执行,即进入到运行状态。注:就 绪状态是进入到运行状态的唯一入口,也就是说,线程要想进入运行状态执行,首先必须处于就绪状态中;
-
阻塞状态(Blocked):处于运行状态中的线程由于某种原因,暂时放弃对CPU的使用权,停止执行,此时进入阻塞状态,直到其进入到就绪状态,才 有机会再次被CPU调用以进入到运行状态。根据阻塞产生的原因不同,阻塞状态又可以分为三种:
1.等待阻塞:运行状态中的线程执行wait()方法,使本线程进入到等待阻塞状态;
2.同步阻塞 – 线程在获取synchronized同步锁失败(因为锁被其它线程所占用),它会进入同步阻塞状态;
3.其他阻塞 – 通过调用线程的sleep()或join()或发出了I/O请求时,线程会进入到阻塞状态。当sleep()状态超时、join()等待线程终止或者超时、或者I/O处理完毕时,线程重新转入就绪状态。 -
死亡状态(Dead):线程执行完了或者因异常退出了run()方法,该线程结束生命周期。
线程的优先级
每一个 Java 线程都有一个优先级,这样有助于操作系统确定线程的调度顺序。
Java 线程的优先级是一个整数,其取值范围是 1 (Thread.MIN_PRIORITY ) - 10 (Thread.MAX_PRIORITY )。
默认情况下,每一个线程都会分配一个优先级 NORM_PRIORITY(5)。
具有较高优先级的线程对程序更重要,并且应该在低优先级的线程之前分配处理器资源。但是,线程优先级不能保证线程执行的顺序,而且非常依赖于平台。
线程的创建
Java 提供了三种创建线程的方法:
1:通过实现 Runnable 接口;
2:通过继承 Thread 类本身;
3:通过 Callable 和 Future 创建线程。
-
继承Thread类
继承Thread类,然后重新run方法,直接调用start方法即可。
class ThreadDemo extends Thread{
String threadName;
public ThreadDemo(String threadName){
this.threadName = threadName;
}
// 重写run方法,作为线程的执行单元
public void run(){
System.out.println("Running: "+threadName);
try{
for (int i = 1; i < 6; i++) {
System.out.println(threadName +" -> "+ i);
Thread.sleep(100);
}
}catch(InterruptedException e){
System.out.println("Thread "+threadName +" is interrupted.");
}
System.out.println("Thread " + threadName +" ending.");
}
}
-
通过实现Runnable接口
通过实现Runnable接口,然后重写start方法,与继承Thread想相比,这种方法可以继承其他类,实现多重继承
class RunnableDemo implements Runnable{
private String threadName;
private Thread thread;
public RunnableDemo(String threadName){
this.threadName = threadName;
}
@Override
public void run() {
System.out.println("Running: "+threadName);
try {
for (int i = 0; i < 6; i++) {
System.out.println(threadName +" -> "+ i);
Thread.sleep(100);
}
} catch (InterruptedException e) {
System.out.println("Thread "+threadName +" is interrupted.");
}
System.out.println("Thread " + threadName +" ending.");
}
// 自己定义的start方法,使用本对象作为Thread的参数,调用Thread的start方法
public void start(){
if(thread == null){
thread = new Thread(this,threadName);
thread.start();
}
}
}
-
通过 Callable 和 Future 创建线程
当我们需要线程运行的时候需要有返回值的时候,我们可以考虑采取这种方式
- 创建 Callable 接口的实现类,并实现 call() 方法,该 call() 方法将作为线程执行体,并且有返回 值。
- 创建 Callable 实现类的实例,使用 FutureTask 类来包装 Callable 对象,该 FutureTask 对象封 装了该 Callable 对象的 call() 方法的返回值。
- 使用 FutureTask 对象作为 Thread 对象的 target 创建并启动新线程。
- 调用 FutureTask 对象的 get() 方法来获得子线程执行结束后的返回值。
class CallableThreadTest implements Callable<Integer> {
public void start(){
CallableThreadTest ctt = new CallableThreadTest();
FutureTask<Integer> ft = new FutureTask<Integer>(ctt);
for(int i = 0;i < 100;i++) {
System.out.println(Thread.currentThread().getName()+" 的循环变量i的值"+i);
if(i==20) {
new Thread(ft,"有返回值的线程").start();
}
}
try {
System.out.println("子线程的返回值:"+ft.get());
} catch (InterruptedException e) {
e.printStackTrace();
} catch (ExecutionException e) {
e.printStackTrace();
}
}
// 重写call方法
@Override
public Integer call() throws Exception {
int i = 0;
for(;i<100;i++) {
System.out.println(Thread.currentThread().getName()+" "+i);
}
return i;
}
}
- 调用
public class 创建线程 {
public static void main(String[] args) {
// 使用继承实现的线程
// ThreadDemo t1 = new ThreadDemo("Thread-1");
// ThreadDemo t2 = new ThreadDemo("Thread-2");
// t1.start();
// t2.start();
// 使用实现接实现的线程
// RunnableDemo runnableDemo1 = new RunnableDemo("Thread-3");
// RunnableDemo runnableDemo2 = new RunnableDemo("Thread-4");
// runnableDemo1.start();
// runnableDemo2.start();
// 使用cell的方式
CallableThreadTest callableThreadTest = new CallableThreadTest();
callableThreadTest.start();
}
}
运行结果
数据共享
当我们采用普通方法时候
class TicketWindow extends Thread{
private int index;
private static final int MAX = 10;
private String windowNmae;
public TicketWindow(String windowNmae){
this.windowNmae = windowNmae;
}
public void run(){
while(index<=MAX){
System.out.println(windowNmae+"出售票:"+index++);
try {
Thread.sleep(10);
} catch (InterruptedException e) {
e.printStackTrace();
}
}
}
}
public class 数据共享与线程同步 {
public static void main(String[] args) {
TicketWindow ticketWindow1 = new TicketWindow("窗口1");
TicketWindow ticketWindow2 = new TicketWindow("窗口2");
TicketWindow ticketWindow3 = new TicketWindow("窗口3");
ticketWindow1.start();
ticketWindow2.start();
ticketWindow3.start();
}
}
运行结果:可以看到运行结果同一张票被出售了多次
为了使得票能够在多个线程中实现数据共享,于是我们采用了static和实现runnable接口两种方式
// 对index进行了static修饰实现变量的共享
class TicketWindow1 extends Thread{
static HashMap mp = new HashMap<Integer,Boolean>();
private static int index;
private static final int MAX = 500;
private String windowNmae;
public TicketWindow1(String windowNmae){
this.windowNmae = windowNmae;
}
public void run(){
while(index<=MAX){
if (mp.containsKey(index))
System.out.println("----------------冲突了,编号:"+index);
else mp.put(index,true);
System.out.println(windowNmae+"出售票:"+index);
index++;
}
}
}
// 使用static进行数据共享的时候只实现是部分数据的共享
// 但是我们要实现很多数据的共享呢,而且static修饰之后对象的生命周期会变得很长
// 对此,我们使用实现runnable接口实现数据共享
class TicketWindow2 implements Runnable{
private int max = 10;
private int index = 0;
@Override
public void run() {
while(index<=max){
System.out.println("出售票:"+index++);
}
}
}
public class 数据共享与线程同步 {
public static void main(String[] args) {
for (int i=1; i<=50; i++)
new TicketWindow1("窗口"+i).start();
// 可以看到在小数据的时候基本是不存在冲突的,也就是说每张票只被出售了一次
// 但是数据(MAX)变大的时候,仍然存在同张票被出售2次了,因为可能同时两个值相同的index进入到运算栈里
// TicketWindow2 t = new TicketWindow2();
// Thread thread1 = new Thread(t);
// Thread thread2 = new Thread(t);
// thread1.start();
// thread2.start();
// 可以看到在小数据的时候基本是不存在冲突的,也就是说每张票只被出售了一次
// 可惜的是,当数据变大的时候,这种方式和static一样,会出现大量数字未被使用或者使用多次的情况
// 所以说这两种方式是线程不安全的
}
}
运行结果:这两种方式都是可以实现数据共享的,但是遗憾的是,他们都无法做到数据同步,也就是线程不安全了。那么我们接下来我们就讨论如何让数据同步。
线程同步
对多线程共享的数据称为临界资源或同步资源
而把访问同步资源的那部分代码称为临界资源或临界区
为了确保临界区只被一个线程执行,Java采用了互斥锁的机制
当一个线程获得互斥锁的时候,其他线程就不能捕获得该锁,只能等该线程释放后再获得
这与之前并发交替执行代码的方式是不同的,这是一种串行的方式
这里的例子,是银行取款的简单样例,我们对take方法上锁了,每次只运行一个线程对该临界区进行访问,保证了里面的数据对多个线程来说是同步的。
class Mbank{
// 共享数据
private static int sum = 2000;
public synchronized static void take(int k){
// public static void take(int k){ // 如果采用这条语句的话sum同一个值会被使用多次,是不安全的
int temp = sum;
temp -= k;
if(temp<=0){
System.out.println("余额不足,请充值");
return;
}
try {
Thread.sleep(200);
} catch (InterruptedException e) {
e.printStackTrace();
}
sum = temp;
System.out.println("sum = " + sum);
}
}
class Customer extends Thread{
public void run(){
for(int i=1; i<=500; i++)
Mbank.take(100);
}
}
public class 数据共享与线程同步2 {
public static void main(String[] args) {
Customer customer1 = new Customer();
Customer customer2 = new Customer();
customer1.start();
customer2.start();
}
}
运行结果:这是使用上面的上锁的方法执行的,如果使用不上锁将会出现数据错乱,读者可以打开注释自行尝试
线程之间的通信
线程的通信必须在synchronized锁定的区域内执行
实现两个线程,当一个线程存入一张票的时候,另一个线程取出一张票
class Tickets{
protected int size;
public int number = 0;
boolean available = false;
public Tickets(int size){
this.size = size;
}
public synchronized void put(){
if(available) { // 如果有票的话
try {
wait(); // 等待,进入阻塞状态
} catch (InterruptedException e) {
e.printStackTrace();
}
}
System.out.println("存入第: " + (++number) + " 号票");
available = true; // 已经存入票了
notify(); // 通知取票线程售票
}
public synchronized void sell(){
if(!available) { // 如果没票的话
try {
wait(); // 等待,进入阻塞线程
} catch (InterruptedException e) {
e.printStackTrace();
}
}
System.out.println("售出第: " + (number) + " 号票");
available = false;
notify(); // 售票完毕后唤醒村票线程
if(number==size)number = size+1; // 最多出售size张票
}
}
// 存票线程
class Producer extends Thread{
Tickets t = null;
public Producer(Tickets t){
this.t = t;
}
public void run(){
while(t.number < t.size){
t.put();
}
}
}
// 售票线程
class Consumer extends Thread{
Tickets t = null;
public Consumer(Tickets t){
this.t = t;
}
public void run(){
while(t.number <= t.size){
t.sell();
}
}
}
public class 线程之间的通信 {
public static void main(String[] args) {
Tickets t = new Tickets(10);
Consumer consumer = new Consumer(t);
Producer producer = new Producer(t);
consumer.start();
producer.start();
}
}
运行结果
附录
Thread类的一些重要方法
方法 | 描述 |
---|---|
public void start() | 使该线程开始执行;Java 虚拟机调用该线程的 run 方法。 |
public void run() | 如果该线程是使用独立的 Runnable 运行对象构造的,则调用该 Runnable 对象的 run 方法;否则,该方法不执行任何操作并返回。 |
public final void setName(String name) | 改变线程名称,使之与参数 name 相同。 |
public final void setPriority(int priority) | 更改线程的优先级。 |
public final void setDaemon(boolean on) | 将该线程标记为守护线程或用户线程。 |
public final void join(long millisec) | 等待该线程终止的时间最长为 millis 毫秒。 |
public void interrupt() | 中断线程。 |
public final boolean isAlive() | 测试线程是否处于活动状态。 |
Thread类的静态方法
方法 | 描述 |
---|---|
public static void yield() | 暂停当前正在执行的线程对象,并执行其他线程。 |
public static void sleep(long millisec) | 在指定的毫秒数内让当前正在执行的线程休眠(暂停执行),此操作受到系统计时器和调度程序精度和准确性的影响。 |
public static boolean holdsLock(Object x) | 当且仅当当前线程在指定的对象上保持监视器锁时,才返回 true。 |
public static Thread currentThread() | 返回对当前正在执行的线程对象的引用。 |
public static void dumpStack() | 将当前线程的堆栈跟踪打印至标准错误流。 |
小结
- 线程是指程序的运行流程。"多线程"的机制可以同时运行好几个程序块,使得程序的运行的效率更高,也可以可以传统程序语言无法解决的问题。
- 多线程与多任务是两个不同的概念,多任务是针对操作系统而言的,表示操作系统可以同时运行多个应用程序;而多线程是针对一个程序而言的,表示在一个程序内部可以同时执行多个线程。
- 创建线程有三种方法:传统的两种中,一种是继承java.lang的Thread类;另一种是用户在定义自己的类中实现Runnable接口。
- run()方法给出了线程要执行的任务。若是派生自Thread类,必须把线程的程序代码编写在run()方法内,实现覆盖操作;若是实现Runnable接口,必须在实现Runnable接口的类里定义run()方法。
- 每一个线程,在创建和消亡之前,均会处于下列五种状态之一:新建状态,就绪状态,运行状态,阻塞状态,消亡状态。
- 阻塞状态一般来自,该线程调用对象的wait()方法,调用sleep方法,和另外一个线程join()在一起,有优先级更高的线程处于就绪状态。
- 线程在运行时,因不需要外部的数据或方法,就不必关心其他线程的状态或行为,这样的线程称为独立,不同步或者异步执行的。
- 被多个线程共享的数据在同一时刻只允许一个线程处于操作之中,这就是线程控制中的线程间互斥。
- synchronized 锁定的是一个具体对象,通常是临界区对象。所有锁定同一个对象的线程之间在synchronized代码上是互斥的,也就是说这些线程的synchronized代码之间是串行执行的,不再是互相穿插并发执行,因为保证了synchronized代码块操作的原子性。
- 由于所有锁定同一个对象的线程之间,在synchronized代码块上是互斥的,这些线程的synchronized代码之间是串行执行的,故锁定的代码数量越好越好,否则多线程就会失去很多并发执行的优秀,而且synchronized是个比较重的操作。
- 一定要保证所有临界区共享的访问与操作均在syncgronized 代码块中执行。