java—多线程
进程与线程的区别:一个进程可以有多个线程
****进程:****是并发执行的程序在执行过程中分配和管理资源的基本单位,是一个动态概念,竞争计算机系统资源的基本单位。
正在运行中的应用程序,通常称为进程。每个进程都有自己独立的地址空间(内存空间),每当用户启动一个进程时,操作系统就会为该进程分配一个独立的内存空间,让应用程序在这个独立的内存空间中运行。
****线程:****是进程的一个执行单元,是进程内科调度实体。比进程更小的独立运行的基本单位。线程也被称为轻量级进程。线程是一个轻量级的子进程,是最小的处理单元;是一个单独的执行路径。可以说:线程是进程的子集(部分)。
****关系:****一个程序至少一个进程,一个进程至少一个线程。
线程的创建方式
java中常见的创建方式有三种:
- 继承Thread类创建线程类
- 继承Runnable接口创建线程类
- 通过Callable和Future创建线程
继承Thread类创建线程类
继承Thread类并重写run方法。
public class Demo1 {
public static void main(String[] args) throws IOException, ClassNotFoundException {
MyThread thread1 = new MyThread(), thread2 = new MyThread();
thread1.start();
thread2.start();
}
}
class MyThread extends Thread {
@Override
public void run() {
for(int i=0;i<5;i++) {
System.out.println("这是"+Thread.currentThread().getName()+"第"+i+"次输出");
try {
Thread.sleep(20);
} catch (InterruptedException e) {
e.printStackTrace();
}
}
}
}
继承Runnable接口创建线程类
继承Runnable接口并实现run方法。然后将该类的对象作为参数传入Thread的构造方法中,然后调用Thread对象的start方法
public class Demo1 {
public static void main(String[] args) throws IOException, ClassNotFoundException {
MyThread thread1 = new MyThread();
Thread t1 = new Thread(thread1), t2 = new Thread(thread1);
t1.start();
t2.start();
}
}
class MyThread implements Runnable {
int i=0;
@Override
public void run() {
for(;i<10;i++) {
System.out.println(Thread.currentThread().getName()+"正在输出:"+i);
//Thread.sleep(20);
}
}
}
通过Callable和Future创建线程
先创建一个继承了Callable接口的类,将该类的实例对象作为参数传入FutureTask构造函数,然后将FutureTask对象作为参数传入Thread构造方法。最后调用Thread的start方法。
public static void main(String[] args) throws IOException, ClassNotFoundException, ExecutionException, InterruptedException {
MyThread myThread = new MyThread();
FutureTask<Integer> futureTask = new FutureTask<>(myThread);
Thread thread = new Thread(futureTask);
for(int i=0;i<10;i++) {
if(i==5) {
thread.start();
}
}
System.out.println("线程的返回值为:"+futureTask.get());
}
}
class MyThread implements Callable<Integer> {
@Override
public Integer call() throws Exception {
System.out.println(Thread.currentThread().getName()+"正在执行!");
return 55;
}
}
三种方式的对比
由于java只实行类的单继承,所以对于继承了Thread类的类而言就不能再继承其他类了,但是实现Runnable或者Callable接口后任可以继承其他类。在这种方式下,多个线程可以共享同一个target对象,所以非常适合多个相同线程来处理同一份资源的情况,从而可以将CPU、代码和数据分开,形成清晰的模型,较好地体现了面向对象的思想。
在开发中,优先选择实现Runnable接口的方式,如果需要有返回值,则实现Callable接口。
注意!!!:关于start和run,thread.run()只是调用一个方法,而thread.start()会启用一个线程。
线程的相关方法
- start:启动当前线程并调用当前线程的run方法
- getName() :获取线程的名字
- setName:设置线程的名字
- yield:释放当前CPU的执行权,当当前线程任能继续参与执行权的抢夺
- join:在a线程中调用b.join()后a线程进入阻塞状态,直到b线程结束,也可以在join中加入1个参数p1,代表最多等待p1毫秒。或者是加入两个参数p1,p2,代表最多等待p1毫秒 加 p2 纳秒
- sleep:让当前线程停止执行一段时间,如果是一个参数,代表毫秒,如果是两个参数代表毫秒和纳秒。
- isAlive判断这个线程是否还活着
- activeCount:返回当前程序中的活动线程数 就绪+阻塞+运行
- currentThread:静态方法,返回执行当前代码的线程
- wait:这是Object类的方法。使当前线程等待,直到其他线程调用此对象的notify或notifyAll方法,或者是指定的等待时间已过
线程的生命周期
线程的生命周期有五种状态:
- 新建 New
- 就绪 Runnable
- 运行 running
- 阻塞 Blocked
- 死亡 Dead
多线程的安全问题与解决办法
产生安全问题的原因
试想这样一种情况,假如一共有10张票,两个线程同时售卖,对于每个线程的过程如下所示:
- 首先判断是否还有票
- 如果没有票了,直接结束
- 如果还有票,卖出一张票,然后将票数减一
问题就出在判断的这里,假如已经,卖了9张票了,还剩最后一张,线程A首先进行判断,发现还有票,所以进入第二个判断体。但是A还没将票数减一的时候B也开始进行判断,发现还有票,同样进入卖票的程序,这样就会导致卖出了11张票,最终的票数为-1,这显然是不合理的。
解决办法:
对于共享变量,同时只能让一个线程修改。也就是同步
1)同步代码块
即让某段代码只能同时被一个线程执行
while(true) {
synchronized (t1) {
if(tickets>0) {
try {
Thread.sleep(100);
} catch (InterruptedException e) {
e.printStackTrace();
}
System.out.println(Thread.currentThread().getName() + "正在出售第" + tickets-- + "张票");
}
else {
Thread.currentThread().interrupt();
}
}
}
t1可以是任意一个对象,我这里是Interger t1 = new Integer(0);
由于两个线程的同步对象都是t1,所以这两个线程同时只有一个能够执行这个代码块。
2)同步方法
其实就是用关键词synchronized修饰一个方法。被修饰的方法同时只能被一个线程执行。
3)lock锁
在java.util.concurrent.locks.lock包下有一个Lock接口,ReentrantLock是他的一个实现类。我们主要使用
- lock 获得锁
- unlock 释放锁的操作放在finally块中进行,以保证锁一定被被释放,防止死锁的发生。
方法就行了,在lock与unlock之间的就是同步代码块。
同步是一种高开销的操作,因此应该尽量减少同步的内容。 通常没有必要同步整个方法,使用synchronized代码块同步关键代码即可。