多线程基础
一、基本概念
进程是执行程序的依次执行过程,它是一个动态的概念。是系统资源分配的单位。
一个进程中可以包含多个线程。线程是CPU调度和执行的单位。
多线程是指有多个CPU,即多核。
二、线程的创建
常见线程有三种方式:继承Thread类、实现Runnable接口、实现Callable接口
1、继承Thread类
步骤:
- 自定义线程类继承Thread类
- 重写run()方法,编写线程执行体
- 创建线程对象,调用start()方法启动线程
public class TestThread1 extends Thread{
@Override
public void run() {
// run方法线程体
for (int i = 0; i < 20 ; i++) {
System.out.println(Thread.currentThread().getName() + "---hello");
}
}
public static void main(String[] args) {
// main 线程,主线程
// 创建一个线程对象
TestThread1 testThread1 = new TestThread1();
// 调用start方法,开启线程
testThread1.start();
for (int i = 0; i < 200 ; i++) {
System.out.println(Thread.currentThread().getName() + "---Word---"+i);
}
}
}
注意:线程开启不一定立即执行,有CPU调度执行
2、实现Runnable接口
步骤:
- 定义MyRunnable类实现Runnable接口
- 实现run()方法,编写线程执行体
- 创建线程对象,调用start()方法启动线程
public class TestThread2 implements Runnable{
@Override
public void run() {
// run方法线程体
for (int i = 0; i < 20 ; i++) {
System.out.println(Thread.currentThread().getName() + "---hello");
}
}
public static void main(String[] args) {
//创建Runnable接口的实现类对象
TestThread2 testThread2 = new TestThread2();
// 创建线程对象,通过线程对象来开启我们的线程,代理
new Thread(testThread2).start();
for (int i = 0; i < 200 ; i++) {
System.out.println(Thread.currentThread().getName() + "---Word---"+i);
}
}
}
考虑一个问题,既然有了继承Thread类来创建线程,为什么会还会有实现Runnable接口?
- 可以解决继承Thread类后不能在继承其他的类的单继承问题。
- Runnable实现的多线程的程序类可以更更好的描述出程序共享的概念 。
3、实现Callable接口
步骤:
- 实现Callable接口,需要返回值类型
- 重写call方法,需要抛出异常
- 创建目标对象
- 创建执行服务:ExecutorService ser = Executors.newFixedThreadPool(1);
- 提交执行:Future result1 = ser.submit(t1);
- 获取结果:boolean r1 = result1.get();
- 关闭服务:ser.shutdownNow();
public class TestThread4 {
private static void test1() throws ExecutionException, InterruptedException {
Callable<String> callable = new Callable<String>() {
@Override
public String call() throws Exception {
System.out.println(Thread.currentThread().getName()+":call");
return "call finish";
}
};
ExecutorService pool = Executors.newFixedThreadPool(1);
Future future = pool.submit(callable);
System.out.println(Thread.currentThread().getName()+ ": pool submit ,before get()" );
System.out.println(future.get());
pool.shutdownNow();
}
public static void main(String[] args) throws ExecutionException, InterruptedException {
test1();
}
}
思考:有了实现Runnable接口已经解决了单继承问题,为神马还会有实现Callable接口?
Callable的好处:
- 可以定义返回值
- 可以抛出异常
三、多线程的方法
1、线程休眠(sleep方法)
-
线程休眠:指的是让线程暂缓执行一下,等到了预计时间之后在恢复执行。
-
注意:线程休眠会交出CPU,让CPU去执行其他的任务,但是有一点非常重要,sleep方法不会释放锁,也就是说,如果当前线程持有某个对象的锁,则即使调用的sleep方法,其他的线程也无法获得这个对象。
-
休眠时间使用毫秒作为单位
public class ThreadSleep {
public static void main(String[] args) {
MyThread mt = new MyThread();
new Thread(mt).start();
new Thread(mt).start();
new Thread(mt).start();
}
}
class MyThread implements Runnable {
@Override
public void run() {
for (int i = 0; i < 1000 ; i++) {
try {
Thread.sleep(1000);
} catch (InterruptedException e) {
// TODO Auto-generated catch block
e.printStackTrace();
}
System.out.println("当前线程: " +Thread.currentThread().getName()+" ,i = " +i);
}
}
}
2、线程让步(yield方法)
-
暂停当前正在执行的线程对象,并执行其他线程。
意思就是调用yield方法会让当前线程交出CPU权限,让CPU去执行其他的线程。它跟sleep方法类似,同样 不会释放锁。但是yield不不能控制具体的交出CPU的时间,另外, yield方法只能让拥有相同优先级的线程有获取CPU执行时间的机会。
-
注意:
调用yield方法并不会让线程进入阻塞状态,而是让线程重回就绪状态,它只需要等待重新获取CPU执行时间,这一点是和sleep方法不一样的 。
-
线程礼让不一定成功,看CPU的心情。
public class ThreadYieldTest {
public static void main(String[] args) {
MyThread mt = new MyThread();
new Thread(mt).start();
new Thread(mt).start();
new Thread(mt).start();
}
}
class MyThread implements Runnable {
@Override
public void run() {
for (int i = 0; i < 3 ; i++) {
Thread.yield();
System.out.println("当前线程: " +
Thread.currentThread().getName()+" ,i = " +i);
}
}
}
3、join方法
等待该线程终止。意思就是如果在主线程中调用该方法时就会让主线程休眠,让调用该方法的线程run方法先执行完毕之后,再开始执行主线程。
import java.text.DateFormat;
import java.text.SimpleDateFormat;
import java.util.Date;
public class ThreadJoinTest {
public static void main(String[] args) throws InterruptedException {
MyThread mt = new MyThread();
Thread thread = new Thread(mt,"子线程A");
thread.start();
System.out.println(Thread.currentThread().getName());
thread.join();
System.out.println("代码结束");
}
public static void printTime() {
Date date=new Date();
DateFormat format=new SimpleDateFormat("yyyy-MM-dd HH:mm:ss");
String time=format.format(date);
System.out.println(time);
}
}
class MyThread implements Runnable {
@Override
public void run() {
try {
System.out.println("主线程休眠前的时间");
ThreadJoinTest.printTime();
Thread.sleep(2000);
System.out.println(Thread.currentThread().getName());
System.out.println("睡眠结束的时间");
ThreadJoinTest.printTime();
} catch (Exception e) {
e.printStackTrace();
}
}
}
4、线程停止
多线程中线程停止有三种方式:
- 设置标志位,可以使线程正常退出!
- 使用stop方法强制使线程退出,但是这个方法不太安全已经被抛弃了!
- 使用Thread类中的
interrupt()
可以中断线程!
4.1、设置标志位使线程正常退出
public class ThreadExist1 {
public static void main(String[] args) throws InterruptedException {
MyThread myThread = new MyThread();
Thread thread1 = new Thread(myThread,"子线程A");
thread1.start();
Thread.sleep(2000);
myThread.setFlag(false);
System.out.println("代码结束");
}
}
class MyThread implements Runnable {
private boolean flag = true;
@Override
public void run() {
int i = 1;
while (flag) {
try {
Thread.sleep(1000);
System.out.println("第"+i+"次执行,线程名称为:"+Thread.currentThread().getName());
i++;
} catch (InterruptedException e) {
// TODO Auto-generated catch block
e.printStackTrace();
}
}
}
public void setFlag(boolean flag) {
this.flag = flag;
}
}
4.2、使用stop方法退出线程
public class ThreadExist1 {
public static void main(String[] args) throws InterruptedException {
MyThread myThread = new MyThread();
Thread thread1 = new Thread(myThread,"子线程A");
thread1.start();
Thread.sleep(3000);
thread1.stop();
System.out.println("代码结束");
}
}
使用stop方法强制使线程退出,但是该方法不太安全所以已经被弃了。
4.3、interrupt()中断线程
interrupt()方法:不会真正结束线程,再当前线程中打上一个停止的标记。
Thread类中的interrupted()方法:测试当前线程是否中断!该方法有检测中断并清除中断状态的作用
isInterrputed()方法:测试线程是否已经中断!
public class ThreadinterruptTest {
public static void main(String[] args) {
Thread thread = new ThreadinterruptTestThread();
thread.start();
thread.interrupt();
System.out.println("是否停止?1" + thread.isInterrupted());
System.out.println("是否停止?2" + Thread.interrupted());
}
}
class ThreadinterruptTestThread extends Thread {
@Override
public void run() {
for (int i = 0; i < 1000 ; i++) {
System.out.println("i+ " + i);
}
}
}
运行结果:
threadtest.ThreadinterruptTest
i+ 0
是否停止?1true
i+ 1
i+ 2
i+ 3
i+ 4
是否停止?2false
i+ 5
...
thread.isInterrupted()方法:是检查ThreadinterruptTestThread是否被打上了停止的标记,
Thread.interrupted()方法:是检查主线程是否被打上了停止标记!
public static void main(String[] args) {
Thread.currentThread().interrupt();
System.out.println("是否停止?1" + Thread.interrupted());
System.out.println("是否停止?2" + Thread.interrupted());
}
运行结果:
是否停止?1true
是否停止?2false
测试当前线程是否已经中断,线程的中断状态由方法清除。如果两次连续调用,第二次调用将返货false!
interrupted()方法有检测中断并清除中断状态的作用
public class ThreadinterruptTest2 {
public static void main(String[] args) throws InterruptedException {
Thread t = new ThreadinterruptTest2Thread();
t.start();
t.interrupt();
}
}
class ThreadinterruptTest2Thread extends Thread {
@Override
public void run() {
try {
for (int i = 0; i < 10000; i++) {
if (this.isInterrupted()) {
System.out.println("已经是停止状态了!我有与退出");
throw new Exception();
}
System.out.println("i : " +i);
}
System.out.println("这里是结束循环后的代码");
}catch (Exception e) {
e.printStackTrace();
}
}
}
运行结果:
public class ThreadinterruptTest3 {
public static void main(String[] args) {
Thread t = new ThreadinterruptTest3Thread();
t.start();
t.interrupt();
}
}
class ThreadinterruptTest3Thread extends Thread {
@Override
public void run() {
try {
for (int i = 0; i < Integer.MAX_VALUE; i++) {
String s = new String();
}
System.out.println("开始线程");
Thread.sleep(20000);
System.out.println("结束线程");
}catch (Exception e) {
System.out.println("进入异常代码块");
e.printStackTrace();
}
}
}
如果程序中有sleep代码,不管是否进入到sleep的状态,如果调用了interrupt方法都会长生异常信息!
总结:
-
interrupt()是给线程设置中断标志
-
interrupted()是检测当前线程是否中断 并 清除中断状态 ;
-
isInterrupted()只检测中断
注意:
interrupted()作用于当前线程 !
interrupt()和isInterrupted()作用于此线程 , 即代码中调用此方法的实例所代表的线程 !
4.5、守护线程
守护线程是一种特殊的线程,它属于是一种陪伴线程。简单点说 java 中有两种线程:用户线程和守护
线程。可以通过 isDaemon() 方法来区别它们:如果返回 false ,则说明该线程是“用户线程”;否则就是“守护线程”。典型的守护线程就是垃圾回收线程。只要当前JVM进程中存在任何一个非守护线程没有结束,守护线程就在工作;只有当最后一个非守护线程结束时,守护线程才会随着JVM一同停止工作。
注意:主线程main是用户线程。
public class ThreadDaeemonTest {
public static void main(String[] args) throws InterruptedException {
Thread thread1 = new Thread(new A(),"子线程A");
// 设置线程A为守护线程,此语句句必须在start⽅方法之前执⾏行行
thread1.setDaemon(true);
thread1.start();
Thread thread2 = new Thread(new A(),"子线程B");
thread2.start();
Thread.sleep(3000);
// 中断⾮非守护线程
thread2.interrupt();
Thread.sleep(10000);
System.out.println("代码结束");
}
}
class A implements Runnable{
private int i;
@Override
public void run() {
try {
while (true) {
i ++ ;
System.out.println("线程名称: " + Thread.currentThread().getName() + ",i=" + i
+ ",是否为守护线程:" + Thread.currentThread().isDaemon());
Thread.sleep(1000);
}
}catch (Exception e) {
System.out.println("线程名称: " +
Thread.currentThread().getName() + "中断线程了了");
}
}
}
运行结果:
从上面结果可以看出来, B是用户线程当它中断了之后,守护线程还没有结束,是因为主线程(用户线
程)还没有结束,所以说明是所有的用户线程结束之后守护线程才会结束。
四、线程的同步问题
1、同步问题的引出
需求:多个线程同时卖票
public class TestDemo {
public static void main(String[] args) {
MyThread t = new MyThread();
new Thread(t, "黄牛A").start();
new Thread(t, "黄牛B").start();
new Thread(t, "黄牛C").start();
}
}
class MyThread implements Runnable{
private int tickets = 10;
@Override
public void run() {
while (tickets > 0) {
try {
Thread.sleep(200);
}catch (Exception e) {
e.printStackTrace();
}
System.out.println(Thread.currentThread().getName()+",还有"+tickets--+"张票");
}
}
}
运行上面的代码,我们发现出现了问题,竟然有的票被卖了多次,还卖出了负票,这显然是不合理的,我们把这个称之为不同步操作
那么怎么解决呢?
2、同步处理
所谓的同步指的是所有的线程不是一起进入到方法中执行,而是按照顺序一个一个进来。
synchronized处理理同步问题
如果要想实现这把"锁"的功能,可以采用关键字synchronized来处理!
使用synchronized关键字处理理有两种模式:同步代码块、同步方法
- 使用同步代码块 : 如果要使用同步代码块必须设置一个要锁定的对象,所以一般可以锁定当
前对象:this
代码实现:
public class TestDemo {
public static void main(String[] args) {
MyThread t = new MyThread();
new Thread(t, "黄牛A").start();
new Thread(t, "黄牛B").start();
new Thread(t, "黄牛C").start();
}
}
class MyThread implements Runnable{
private int tickets = 10;
@Override
public void run() {
synchronized (this) {
while (tickets > 0) {
try {
Thread.sleep(200);
} catch (Exception e) {
e.printStackTrace();
}
System.out.println(Thread.currentThread().getName() + ",还有" + tickets-- + "张票");
}
}
}
}
第一种方式是在方法里拦截的,也就是说进入到方法中的线程依然可能会有多个。
- 同步方法:
public class TestDemo {
public static void main(String[] args) {
MyThread t = new MyThread();
new Thread(t, "黄牛A").start();
new Thread(t, "黄牛B").start();
new Thread(t, "黄牛C").start();
}
}
class MyThread implements Runnable{
private int tickets = 10;
@Override
public void run() {
sale();
}
private synchronized void sale() {
while (tickets > 0) {
try {
Thread.sleep(200);
} catch (Exception e) {
e.printStackTrace();
}
System.out.println(Thread.currentThread().getName() + ",还有" + tickets-- + "张票");
}
}
}
同步虽然可以保证数据的完整性(线程安全操作),但是其执行的速度会很慢。
3、关于synchronized的额外说明
先来看一段代码:
范例:观察synchronized锁多对象
public class Test {
public static void main(String[] args) {
for (int i = 0; i < 3 ; i++) {
Thread thread = new MyThread() ;
thread.start();
}
}
}
class Sync {
public synchronized void test() {
System.out.println("test⽅方法开始,当前线程为 "+Thread.currentThread().getName());
try {
Thread.sleep(1000);
} catch (InterruptedException e) {
e.printStackTrace();
}
System.out.println("test⽅方法结束,当前线程为 "+Thread.currentThread().getName());
}
}
class MyThread extends Thread {
@Override
public void run() {
Sync sync = new Sync() ;
sync.test();
}
}
通过上述代码以及运行结果我们可以发现,没有看到synchronized起到作用,三个线程同时运行test()
方法。
实际上, synchronized(this)以及非static的synchronized方法,只能防止多个线程同时执行同一个
对象的同步代码段。即synchronized锁住的是括号里的对象,而不不是代码。对于非static的synchronized方法,锁的就是对象本身也就是this。
当synchronized锁住一个对象后,别的线程如果也想拿到这个对象的锁,就必须等待这个线程执行行完成释放锁,才能再次给对象加锁,这样才达到线程同步的目的。即使两个不同的代码段,都要锁同一个对象,那么这两个代码段也不能在多线程环境下同时运行。
那么,如果真要锁住这段代码,要怎么做?
介绍两种思路
-
第一种也是最容易易想到的,只要锁住同一个对象不不就OK了了?
-
第二种思路也是我们常用的思路,让synchronized锁这个类对应的Class对象
下面分别介绍这两种方法:
3.1、锁住同一个对象
修改上述代码,锁同一个对象:
class Sync {
public void test() {
synchronized (this) {
System.out.println("test⽅方法开始,当前线程为 " +
Thread.currentThread().getName());
try {
Thread.sleep(1000);
} catch (InterruptedException e) {
e.printStackTrace();
}
System.out.println("test⽅方法结束,当前线程为 " +
Thread.currentThread().getName());
}
}
}
class MyThread extends Thread {
private Sync sync ;
public MyThread(Sync sync) {
this.sync = sync ;
}
@Override
public void run() {
this.sync.test();
}
}
public class Test {
public static void main(String[] args) {
Sync sync = new Sync() ;
for (int i = 0; i < 3 ; i++) {
Thread thread = new MyThread(sync) ;
thread.start();
}
}
}
3.2、synchronized锁这个类对应的Class对象
class Sync {
public void test() {
synchronized(Sync.class) {
System.out.println("test⽅方法开始,当前线程为 " +
Thread.currentThread().getName());
try {
Thread.sleep(1000);
} catch (InterruptedException e) {
e.printStackTrace();
}
System.out.println("test⽅方法结束,当前线程为 " +
Thread.currentThread().getName());
}
}
}
class MyThread extends Thread {
@Override
public void run() {Sync sync = new Sync() ;
sync.test();
}
}
public class Test {
public static void main(String[] args) {
for (int i = 0; i < 3 ; i++) {
Thread thread = new MyThread() ;
thread.start();
}
}
}
上面代码用synchronized(Sync.class)实现了了全局锁的效果。因此,如果要想锁的是代码段,锁住多个对象的同一方法,使用这种全局锁,锁的是类而不不是this。
static synchronized方法, static方法可以直接类名加方法名调用,方法中无法使用this,所以它锁的不是
this,而是类的Class对象,所以, static synchronized方法也相当于全局锁,相当于锁住了代码段。