Java–多线程
1.对多线程的基础理解
线程是进程执行的一个路线。在同一个进程中的,线程共享进程的内存空间,线程间可以自由切换,并发执行。在Java开发的过程中,设计的应用程序,往往都是多个线程同时工作的,这样能够提高程序的执行效率,但是也带来了编程的复杂,需要解决多线程之间资源分配的问题。在本篇的博客中,主要记录的是Java实现多线程的几个方式,以及基础的概念和常用的方法操作。
2.创建线程的方式
-
继承Thread类,重写run方法
-
实现Runnable接口,编写run方法
-
实现Callable接口,编写call方法
1) 继承Threead的方式,参考例子如下:
// 定义MyThread继承Thread类型,并重写run方法,在run方法内编写创建的线程要执行的任务。 public class MyThread extends Thread { // 线程要执行的方法 // 通过thread对象的start方法执行,是一条新的执行路径 @Override public void run() { for (int i=0;i<100;i++){ System.out.println("窗前明月光"+i); } } }
// 创建MyThread类,并调用start方法,然后线程会开始执行run方法 public static void main(String[] args) { MyThread myThread = new MyThread(); myThread.start(); for (int i=0;i<100;i++){ System.out.println("疑是地上霜"+i); } }
通过上面运行的程序可以观察到,myThread执行的任务和主线程打印任务是有交叉执行的,说明Java中线程之间的运行是抢占式的,线程抢到资源后,可以执行,没有抢到足够资源的线程需要等待下一次的抢占。
2)实现Runnable的方式,可以参考下面的例子
// 定义MyRunnable实现Runnable接口,并实现run方法,run方法内编写线程执行的任务。 public class MyRunnable implements Runnable { @Override public void run() { for (int i=0;i<20;i++){ System.out.println("锄禾日当午"+i); } } }
// 将MyRunnable对象作为创建Thread对象的参数,然后调用start方法执行线程 public static void main(String[] args) { MyRunnable myRunnable = new MyRunnable(); Thread thread = new Thread(myRunnable); thread.start(); for (int i=0;i<20;i++){ System.out.println("汗滴禾下土"+i); } }
特点:
1. 通过创建任务,给线程分配任务,更适合多个线程同时执行相同的任务; 2. 可以避免单继承的局限性; 3. 任务和线程本身是分离,提高程序的健壮性 4. 线程池的操作,只支持实现Runnable接口的类型的任务,不接受继承Thread的线程
3)实现Callable方法
public static void main(String[] args) throws ExecutionException, InterruptedException {
// 1. 创建MyCallable对象
MyCallable myCallable = new MyCallable();
// 2. 创建FutureTask对象,并将MyCallable对象作为创建对象的参数
FutureTask<Integer> futureTask = new FutureTask<>(myCallable);
// 3. 调用Thread对象的start方法执行线程
new Thread(futureTask).start(); // 执行线程
// 4. 可以通过get方法获取线程执行完成时的返回值
System.out.println(futureTask.get()); // 输出任务返回值
}
特点: 可以携带线程任务执行完成后返回的参数
3. 线程的一些常用方法
-
设置和获取线程的名字
// 设置当前线程的名字 Thread.currentThread().setName("线程1"); // 获取当前线程的名字 Thread.currentThread().getName();
-
线程休眠函数
Thread.sleep(1000); // 单位是ms
-
线程中断
// 终端线程执行可以调用interrupt()函数,通过中断可以终止执行的线程 public static void main(String[] args) throws InterruptedException { Thread t1 = new Thread(new MyRunnable()); t1.start(); for (int i=0;i<5;i++){ System.out.println(Thread.currentThread().getName()+":"+i); Thread.sleep(1000); } t1.interrupt(); // 中断t1线程,通知他进行其他操作,可以是终止自身线程,或者是进行其他的操作 } static class MyRunnable implements Runnable{ @Override public void run() { for (int i=0;i<10;i++){ System.out.println(Thread.currentThread().getName()+":"+i); try { Thread.sleep(1000); } catch (InterruptedException e) { // e.printStackTrace(); // 接收到中断信号时,终止任务/或者开始其他的任务 System.out.println("接收到中断信号时,终止任务"); return; // 终止任务 } } } }
-
设置守护线程
通过调用线程对象的setDaemon()方法,并传入true可以设置线程为守护线程
Java中的线程可以分成用户线程和守护线程,下面是这两种线程的特点;
用户线程,当进程中没有一个用户线程的时候,进程结束
守护线程,守护用户线程,当最后一个用户线程结束时,所有的守护线程死亡举例如下:
public static void main(String[] args) throws InterruptedException { Thread t1 = new Thread(new MyRunnable()); t1.setDaemon(true); // 设置t1为守护线程,如果所有的用户线程结束了,t1守护线程也会停止执行 t1.start(); for (int i=0;i<5;i++){ // 比较快结束 System.out.println(Thread.currentThread().getName()+":"+i); Thread.sleep(1000); } } static class MyRunnable implements Runnable{ @Override public void run() { for (int i=0;i<10;i++){ // 在所有用户线程结束时,作为守护线程的它也会结束 System.out.println(Thread.currentThread().getName()+":"+i); } } }
4.线程同步
在多线程的环境下,线程的执行往往是多样的,当这多个线程在同时对一个数据进行更新操作的时候,可能会因为同时操作的原因,导致数据出现错误,不满足实际的情况。在这种情况下,如果需要保证数据的安全可靠,就要是每个对数据进行更新的线程排队执行,这样才能保证数据不会超出合理的范围,保证每次操作的合理性。这种排队的行为是线程同步的一种具体解释.
Java中为了保证线程安全,使线程同步执行的方式有以下三种:
-
同步代码块
-
同步方法
-
显示锁Lock
下面详细介绍这三种的使用方法,并给出一些例子:
首先给出一个会出现线程不安全的情况,这个例子演示的是多个线程同时进行买票,最后会出现剩余票数是负数的情况,这是不合理的现象。示例如下:
// 模拟线程不安全的情景:买票线程
public static void main(String[] args) {
Ticekt ticekt = new Ticekt(); // 一个对象
new Thread(ticekt).start(); // 三个买票线程
new Thread(ticekt).start();
new Thread(ticekt).start();
}
static class Ticekt implements Runnable{
private int num = 10; // 余票数量
@Override
public void run() {
while (num>0){ // 票数>0,继续卖
System.out.println("准备买出一张票");
try {
Thread.sleep(1000);
} catch (InterruptedException e) {
e.printStackTrace();
}
System.out.println("出票成功!");
num--;
System.out.println("当前余票数:"+num);
}
}
}
1.同步代码块
同步代码块使用关键字synchronize,参数是一个锁对象,然后将同步执行的代码块用{}包围。当遇到同一个锁对象的时候,这些线程会同步执行,如果使不同的锁对象的时候没有任何影响。因此,如果希望同步执行的话,要注意锁对象要是同一个对象。
修改上面不安全的买票程序,做出下面的例子:
// synchronize,利用锁对象,
// 当要进入同一个锁的代码块的时候会先检查这个锁有没有被占用,
// 被占用则等待,否则占用锁进入代码块
public static void main(String[] args) {
Ticekt ticekt = new Ticekt();
new Thread(ticekt).start();
new Thread(ticekt).start();
new Thread(ticekt).start();
}
static class Ticekt implements Runnable{
private int num = 10;
private Object o =new Object(); // 锁对象
@Override
public void run() {
while (true){
synchronized (o){ // 同步代码块开始
if (num>0){
System.out.println("准备买出一张票");
try {
Thread.sleep(1000);
} catch (InterruptedException e) {
e.printStackTrace();
}
System.out.println("出票成功!");
num--;
System.out.println(Thread.currentThread().getName()+":当前余票数:"+num);
}else
break;
} // 同步代码块结束
}
}
}
-
同步方法
同步方法的实现和上面的同步代码块类似,也使用synchronized,但是它修饰的对象变成了方法。当修饰的是普通方法的时候,锁对象是当前对象本身,即this;如果修饰的是静态方法的时候,锁对象则是这个类的字节码文件对象。
下面只做修饰普通方法的举例,修饰静态方法的情景类似:
public static void main(String[] args) { Ticekt ticekt = new Ticekt(); new Thread(ticekt).start(); new Thread(ticekt).start(); new Thread(ticekt).start(); } static class Ticekt implements Runnable{ private int num = 10; @Override public void run() { while (true){ boolean flag = sale(); if (!flag) break; } } // 买票方法,同步执行, // 不是静态方法,锁对象为this,即调用的对象 // 是静态方法,锁对象为Ticke.class,字节码文件对象 // 同一个类的对象会共用this这个锁,可以会影响其他同步方法的执行 public synchronized boolean sale(){ if (num>0){ System.out.println(Thread.currentThread().getName()+":准备买出一张票"); try { Thread.sleep(1000); } catch (InterruptedException e) { e.printStackTrace(); } System.out.println(Thread.currentThread().getName()+":出票成功!"); num--; System.out.println(Thread.currentThread().getName()+":当前余票数:"+num); return true; } return false; } }
-
使用显示锁Lock
同步代码块和同步方法实现都是基于隐式锁。
使用实现接口Lock的类ReentrantLock的方式是基于显示锁。
显示锁比较适合自定义加锁和解锁,带来个性化的同时也可能会带来加锁后忘解锁的情况,在使用的过程中,需要充分考虑到加锁后在哪个地方再解锁,防止程序进入死锁,无法释放资源。下面是使用案例,也是基于买票程序的:
public static void main(String[] args) { Ticekt ticekt = new Ticekt(); new Thread(ticekt).start(); new Thread(ticekt).start(); new Thread(ticekt).start(); } static class Ticekt implements Runnable{ private int num = 10; // 用实现接口Lock的类ReentrantLock创建对象 private Lock l= new ReentrantLock(); @Override public void run() { while (true){ l.lock(); // 上锁资源访问 if (num>0){ System.out.println(Thread.currentThread().getName()+":准备买出一张票"); try { Thread.sleep(1000); } catch (InterruptedException e) { e.printStackTrace(); } System.out.println(Thread.currentThread().getName()+":出票成功!"); num--; System.out.println(Thread.currentThread().getName()+":当前余票数:"+num); l.unlock(); // 操作成功,解锁 }else{ l.unlock(); // 没有买票,break前需要释放锁,防止后面的线程被锁住不能释放 break; } } } }
-
5.总结
以上的介绍,总结了Java中使用多线程的基本内容,包括线程的创建的方式,线程常用的方法,实现线程同步的方法。在我们实际开发中,情景往往都是复杂的,可能会有大量的请求会同时发出,这个时候就需要合理的处理号这些请求,利用多线程的思想去解决这些问题。认识并使用多线程后,能够使自己开发的程序更加符合实际开发的需求,提高程序的执行效率。Java中的多线程的知识还很多,比如线程池。上面的介绍只是基础入门,想要深入学习的话,可以继续查找资料,或者找些项目实践。