概念:
如果一个 进程中同时运行了多个线程,用来完成不同的工作,则称为多线程。
多个线程交替的占用CPU资源,而非真正的并行执行。
好处:
充分利用CPU资源
简化编程模型
带来良好的用户体验
1、获取当前线程
public class ThreadDemo {
public static void main(String[] args) {
//获取当前线程
Thread t=Thread.currentThread();
System.out.println("当前线程是"+t.getName());
t.setName("我是Java主线程");
System.out.println("当前线程的名字是:"+t.getName());
}
}
2、实现线程的三种方法:
1、继承Thread的方式实现线程
Thread类本质上是实现了Runnable接口的一个实例,代表一个线程的实例。启动线程的唯一方法就是通过Thread类的start()实例方法。start()方法是一个native方法,它将启动一个新线程,并执行run()方法。这种方式实现多线程很简单,通过自己的类直接extend Thread,并复写run()方法,就可以启动新线程并执行自己定义的run()方法。
public class MyThreadDemo {
public static void main(String[] args) {
MyThread thread1 = new MyThread(); //创建线程
MyThread thread2 = new MyThread();
thread1.start();
thread2.start();
for(int i=0;i<100;i++) {
System.out.println("main主线程"+i);
}
}
class MyThread extends Thread{
//重写run方法
@Override
public void run(){
for(int i=0;i<100;i++) {
System.out.println(Thread.currentThread().getName()+":"+i);
}
}
}
}
运行结果:
main主线程0
Thread-1:0
Thread-0:0
Thread-1:1
main主线程1
Thread-1:2
Thread-0:1
Thread-1:3
main主线程2
Thread-1:4
Thread-0:2
Thread-1:5
main主线程3
Thread-1:6
Thread-0:3
.......
2、实现Runnable接口实现线程
如果自己的类已经extends另一个类,就无法直接extends Thread,此时,可以实现一个Runnable接口
public class MyRunnableDemo {
public static void main(String[] args) {
MyRunnable myRunnable = new MyRunnable();
//使用静态代理
Thread thread = new Thread(myRunnable);
thread.start();
for(int i=0;i<100;i++) {
System.out.println("main主线程"+i);
}
}
class MyRunnable implements Runnable{
@Override
public void run() {
for(int i=0;i<100;i++) {
System.out.println(Thread.currentThread().getName()+":"+i);
}
}
}
}
为了启动MyRunnable ,需要首先实例化一个Thread,并传入自己的MyRunnable 实例,这里用到了静态代理。
运行结果:
main主线程0
Thread-0:0
main主线程1
Thread-0:1
main主线程2
Thread-0:2
main主线程3
Thread-0:3
main主线程4
Thread-0:4
main主线程5
Thread-0:5
main主线程6
Thread-0:6
main主线程7
Thread-0:7
main主线程8
Thread-0:8
main主线程9
3、实现Callable接口创建Thread线程,然后该类重写 Callable这个接口里的抽象方法call。
Callable接口(也只有一个方法)源码如下:
@FunctionalInterface
public interface Callable<V> {
/**
* Computes a result, or throws an exception if unable to do so.
*
* @return computed result
* @throws Exception if unable to compute a result
*/
V call() throws Exception;
}
Callable 接口类似于 Runnable,两者都是为那些其实例可能被另一个线程执行的类设计的。但是 Runnable 不会返回结果,并且无法抛出经过检查的异常。
案例:通过实现Callable这个接口,来实现多线程,创建一个求和类;
具体的多线程实现代码如下:
package com.stan.thread;
/**
*实现Callable接口实现线程
*/
import java.util.concurrent.Callable;
import java.util.concurrent.ExecutionException;
import java.util.concurrent.ExecutorService;
import java.util.concurrent.Executors;
import java.util.concurrent.Future;
public class CallableDemo {
public static void main(String[] args) throws InterruptedException, ExecutionException {
//创建线程池,线程池里面可以养两个线程
ExecutorService pool = Executors.newFixedThreadPool(2);
//由于下面创建的类中里面有一个有参构造方法,这里得传一个参数
//submit返回一个Future,所以说,应该用Future去接收
//Future可以获取出来1-100或者是1-50的求和结果
Future<Integer> f1 = pool.submit(new MyCallable(100));
Future<Integer> f2 = pool.submit(new MyCallable(50));
//用get()方法获取返回结果
System.out.println(f1.get());
System.out.println(f2.get());
//V get() throws InterruptedException,ExecutionException如有必要,
//等待计算完成,然后获取其结果。
pool.shutdown(); //关闭线程池
}
}
//创建一个MyCallable类去实现Callable这个接口
class MyCallable implements Callable<Integer>{
//设置一个成员变量num
private int num;
//创建一个有参构造方法
public MyCallable (int num){
//num是传进去的数
this.num = num;
}
//重写Callable这个接口里面的抽象方法call;这里的返回值类跟泛型<Integer>是一致的
@Override
public Integer call() throws Exception {
int sum = 0; //实现求和代码
for (int i = 1; i <= num; i++) {
sum += i;
}
return sum; //返回sum
}
}
3、线程常用的方法
1、sleep()
使当前线程(即调用该方法的线程)暂停执行一段时间,让其他线程有机会继续执行,但它并不释放对象锁。也就是说如果有synchronized同步快,其他线程仍然不能访问共享数据。注意该方法要捕捉异常。例如有两个线程同时执行(没有synchronized)一个线程优先级为MAX_PRIORITY,另一个为MIN_PRIORITY,如果没有Sleep()方法,只有高优先级的线程执行完毕后,低优先级的线程才能够执行;但是高优先级的线程sleep(500)后,低优先级就有机会执行了。总之,sleep()可以使低优先级的线程得到执行的机会,当然也可以让同优先级、高优先级的线程有执行的机会。
package cn.guan.thread.status;
/**
* 测试Sleep()
* 1秒输出一次数字
* @author 关启培
*
*/
public class SleepThreads {
public static void main(String[] args) throws InterruptedException {
int num=0;
while(true) {
System.err.println("学习了"+(num++)+"秒");
Thread.sleep(1000);
}
}
2、join()
join()方法使调用该方法的线程执行完毕,也就是等待该方法的线程执行完毕后再往下继续执行。注意该方法也需要捕捉异常。
package cn.guan.thread.status;
/**
* join()方法测试
* @author
*
*/
public class JoinThread extends Thread{
@Override
public void run() {
for(int i=0;i<200;i++) {
System.out.println("join ...."+i);
}
}
public static void main(String[] args) throws InterruptedException {
JoinThread s=new JoinThread();
Thread t=new Thread(s);
t.start();
for(int i=0;i<100;i++) {
if(i==60) {
//t线程加入到main线程中来,只有t线程运行结束,才会继续往下走
t.join();
}
//会观察到直到t线程运行完才执行main线程
System.out.println("main-->"+i);
}
}
}
3、yield()
当前线程,临时暂停,使得其他线程可以有更多的机会占用CPU资源,该方法与sleep()类似,只是不能由用户指定暂停多长时间,并且yield()方法只能让同优先级的线程有执行的机会。
package com.stan.thread;
/**
* 线程礼让
* @author guan
*
*/
public class MyThread02 implements Runnable{
@Override
public void run() {
for(int i=0;i<10;i++) {
System.out.println("线程礼让:"+i);
if(i==6){
//当i为6时,当前线程,临时暂停,使得其他线程占用CPU资源
Thread.yield();
}
}
}
public static void main(String[] args) {
MyThread02 thead1 = new MyThread02();
Thread proxy = new Thread(thead1);
proxy.start();
for(int i=0;i<15;i++) {
System.out.println("主线程:"+i);
}
}
}
4、wait()和notify()、notifyAll()
这三个方法用于协调多个线程对共享数据的存取,所以必须在synchronized语句块内使用。synchronized关键字用于保护共享数据,阻止其他线程对共享数据的存取,但是这样程序的流程就很不灵活了,如何才能在当前线程还没退出synchronized数据块时让其他线程也有机会访问共享数据呢?此时就用这三个方法来灵活控制。
wait()方法使当前线程暂停执行并释放对象锁标示,让其他线程可以进入synchronized数据块,当前线程被放入对象等待池中。当调用notify()方法后,将从对象的等待池中移走一个任意的线程并放到锁标志等待池中,只有锁标志等待池中线程能够获取锁标志;如果锁标志等待池中没有线程,则notify()不起作用。
notifyAll()则从对象等待池中移走所有等待那个对象的线程并放到锁标志等待池中。
注意 这三个方法都是java.lang.Object的方法。
二、run和start()
把需要处理的代码放到run()方法中,start()方法启动线程将自动调用run()方法,这个由java的内存机制规定的。并且run()方法必需是public访问权限,返回值类型为void。
三、关键字synchronized
该关键字用于保护共享数据,当然前提条件是要分清哪些数据是共享数据。每个对象都有一个锁标志,当一个线程访问到该对象,被Synchronized修饰的数据将被"上锁",阻止其他线程访问。当前线程访问完这部分数据后释放锁标志,其他线程就可以访问了。
package com.stan.thread;
/**
* 模拟抢票过程
* @author guan
*
*/
public class Size implements Runnable {
//count就是共享数据
private int count=20;
private int num=0;
@Override
public void run() {
while(true){
//同步代码块
synchronized (this) {
if(count<=0) {
break;
}
//修改数据
num++;
count--;
try {
//让当前线程休眠0.5秒
Thread.sleep(500);
}catch(InterruptedException e) {
e.printStackTrace();
}
System.out.println(Thread.currentThread().getName()+"抢到第"+num+
"张票,剩余"+count+"张");
}
}
}
public static void main(String[] args) {
Size size = new Size();
Thread person1 = new Thread(size,"关启培");
Thread person2 = new Thread(size,"抢票代理");
Thread person3 = new Thread(size,"黄牛");
System.out.println("************开始抢票*************");
person1.start();
person2.start();
person3.start();
}
}
运行结果:
************开始抢票*************
关启培抢到第1张票,剩余19张
黄牛抢到第2张票,剩余18张
抢票代理抢到第3张票,剩余17张
黄牛抢到第4张票,剩余16张
关启培抢到第5张票,剩余15张
黄牛抢到第6张票,剩余14张
黄牛抢到第7张票,剩余13张
抢票代理抢到第8张票,剩余12张
黄牛抢到第9张票,剩余11张
黄牛抢到第10张票,剩余10张
关启培抢到第11张票,剩余9张
黄牛抢到第12张票,剩余8张
黄牛抢到第13张票,剩余7张
抢票代理抢到第14张票,剩余6张
黄牛抢到第15张票,剩余5张
关启培抢到第16张票,剩余4张
关启培抢到第17张票,剩余3张
黄牛抢到第18张票,剩余2张
黄牛抢到第19张票,剩余1张
抢票代理抢到第20张票,剩余0张
四、wait()和notify(),notifyAll()是Object类的方法,sleep()和yield()是Thread类的方法。
(1)、常用的wait方法有wait()和wait(long timeout);
void wait() 在其他线程调用此对象的 notify() 方法或者 notifyAll()方法前,导致当前线程等待。
void wait(long timeout)在其他线程调用此对象的notify() 方法 或者 notifyAll()方法,或者超过指定的时间量前,导致当前线程等待。
wait()后,线程会释放掉它所占有的“锁标志”,从而使线程所在对象中的其他shnchronized数据可被别的线程使用。
wait()h和notify()因为会对对象的“锁标志”进行操作,所以他们必需在Synchronized函数或者 synchronized block 中进行调用。如果在non-synchronized 函数或 non-synchronized block 中进行调用,虽然能编译通过,但在运行时会发生IllegalMonitorStateException的异常。。
(2)、Thread.sleep(long millis)必须带有一个时间参数。
sleep(long)使当前线程进入停滞状态,所以执行sleep()的线程在指定的时间内肯定不会被执行;
sleep(long)可使优先级低的线程得到执行的机会,当然也可以让同优先级的线程有执行的机会;
sleep(long)是不会释放锁标志的。
(3)、yield()没有参数
sleep 方法使当前运行中的线程睡眠一段时间,进入不可以运行状态,这段时间的长短是由程序设定的,yield方法使当前线程让出CPU占有权,但让出的时间是不可设定的。
yield()也不会释放锁标志。
实际上,yield()方法对应了如下操作;先检测当前是否有相同优先级的线程处于同可运行状态,如有,则把CPU的占有权交给次线程,否则继续运行原来的线程,所以yield()方法称为“退让”,它把运行机会让给了同等级的其他线程。
sleep 方法允许较低优先级的线程获得运行机会,但yield()方法执行时,当前线程仍处在可运行状态,所以不可能让出较低优先级的线程此时获取CPU占有权。在一个运行系统中,如果较高优先级的线程没有调用sleep方法,也没有受到I/O阻塞,那么较低优先级线程只能等待所有较高优先级的线程运行结束,方可有机会运行。
yield()只是使当前线程重新回到可执行状态,所有执行yield()的线程有可能在进入到可执行状态后马上又被执行,所以yield()方法只能使同优先级的线程有执行的机会。