Java多线程入门(绝对详细,多Demo帮助了解)
多线程入门
先大致了解下进程与线程
程序运行起来叫进程
进程包含若干线程(默认含有主线程、gc线程)
一、创建线程的三个方法:
- 继承Thread类
package threadTest;
/** * 创建线程方式一 * 继承Thread类 * 重写run()方法 * 调用start()方法启动线程 */
public class ThreadTest extends Thread {
public static void main(String[] args) {
Thread thread = new ThreadTest();
thread.start();
for (int i=0;i<1000;i++){
System.out.println("main线程运行中"+i);
}
}
@Override
public void run() {
for (int i=0;i<100;i++){
System.out.println("Thread子线程运行中"+i);
}
}
}
- 实现Runnable接口
package threadTest;
/** * 创建线程方式二 * 实现Runnable接口 * 实现run()方法 * 创建Thread类传入Runnable实现类对象 * 调用Thread的start()方法 */
public class RunnableTest implements Runnable {
@Override
public void run() {
for (int i = 0; i < 100; i++) {
System.out.println("Runnable创建子线程运行中"+i);
}
}
public static void main(String[] args) {
Thread thread = new Thread(new RunnableTest());
thread.start();
for (int i = 0; i < 1000; i++) {
System.out.println("main线程运行中"+i);
}
}
}
- 实现Callable接口(需要返回值类型,该方法目前仅做了解)
package threadTest;
import java.util.concurrent.*;
/** * 创建线程方式三 * 实现Callable接口(拥有返回值) * 实现call方法,需要抛出异常 * 创建Callable实现类对象 * 创建执行服务:ExecutorService ser = Executors.newFixedThreadPool() * 提交执行线程:Future result = ser.submit(new CallableTest) * 获取结果:boolean res = result.get(); * 关闭服务:ser.shutdownNow(); */
public class CallableTest implements Callable<Boolean> {
@Override
public Boolean call() throws Exception {
for (int i = 0; i < 100; i++) {
System.out.println("Callable创建子线程运行中"+i);
}
return true;
}
public static void main(String[] args) throws ExecutionException, InterruptedException {
CallableTest callableTest = new CallableTest();
ExecutorService ser = Executors.newFixedThreadPool(1);
Future<Boolean> result = ser.submit(callableTest);
for (int i = 0; i < 1000; i++) {
System.out.println("main线程运行中"+i);
}
boolean res = result.get();
ser.shutdownNow();
}
}
Thread方式与Runnable方式比较:
其一:
- Runnable方式可以将程序代码与数据进行有效的分离
- Thread方式则代码与数据具有较高的耦合性
其二:
- Runnable方式可以避免由于Java单继承所带来的局限性
- Thread只能够创建已继承的打单个Thread方式
二、Lambda表达式:
函数式接口:任何接口如果只包含唯一一个抽象方法,那么它就是一个函数式接口
package threadTest;
/** * Lambda表达式 * 前提条件需要一个函数式接口 * 优点:避免内部类定义过多,简化代码 * 仅留下核心逻辑 */
public class LambdaTest {
public static void main(String[] args) {
LambdaInterface lambdaInterface;
lambdaInterface =()->{
System.out.println("Lambda表达式重写使用测试");
};
lambdaInterface.run();
}
}
interface LambdaInterface{
void run();
}
class LambdaImpl implements LambdaInterface{
@Override
public void run() {
System.out.println("Lambda表达式使用测试");
}
}
三、线程状态:
线程的生命周期和状态转换:
线程生命周期共有五个阶段:
- 新建状态:新创建的对象所处状态,此时不能运行,但是JVM为其分配了内存,就和普通java对象一样
- 就绪状态:线程对象调用start()方法后所处状态,此时可运行进入可运行池中,等待CPU调度
- 运行状态:此时线程获取CPU使用权,开始执行run()方法
- 阻塞状态:在某些特殊情况下会放弃CPU使用权,进入阻塞状态
- 举例:
- 当线程试图获取某个对象的同步锁时,如果锁被其他线程持有
- 当线程调用阻塞式的IO方法时
- 调用了某个对象的wait()方法
- 调用了Thread的sleep()方法
- 调用了另一个线程的join()方法
- tip:线程只能从阻塞状态到就绪状态,不能直接进入运行状态
- 死亡状态:线程run()方法执行完毕或抛出未捕获的Exception、错误Error时线程就会进入死亡状态。此时线程不可运行,也不可转换到其他状态
四、线程的调度:
在计算机中,线程调度有两种模型:分时调度模型和抢占式调度模型
分时调度:
线程轮流获取CPU使用权,平均分配每个线程占用的时间片
抢占式调度:
让可运行池中优先级较高的线程优先占用CPU,若优先级相同则随机选择。JVM默认采用抢占式调度
4.1 线程休眠
静态方法sleep(long millis):
- 该方法可以让当前正在执行的线程暂停,进入休眠等待状态
- 该方法抛出InterruptedException异常,使用时需要抛出或捕获
package ThreadMethodTest;
import threadTest.RunnableTest;
public class SleepTest implements Runnable{
@Override
public void run() {
try {
for (int i = 0; i < 100; i++) {
if(i==50){
Thread.sleep(1000);
}
System.out.println("Runnable创建子线程运行中,50会休眠"+i);
}
} catch (InterruptedException e) {
e.printStackTrace();
}
}
public static void main(String[] args) {
Thread thread = new Thread(new SleepTest());
thread.start();
for (int i = 0; i < 1000; i++) {
System.out.println("main线程正运行中"+i);
}
}
}
4.2 线程让步:
静态方法yield():
将当前正在执行的前程暂停,转换为就绪状态,让CPU重新调度一次
package ThreadMethodTest;
public class YieldTest implements Runnable {
@Override
public void run() {
for (int i = 0; i < 100; i++) {
System.out.println("Runnable创建的子线程在运行"+i);
if(i==50){
Thread.yield();
}
}
}
public static void main(String[] args) {
Thread thread = new Thread(new YieldTest());
thread.start();
for (int i = 0; i < 1000; i++) {
System.out.println("main线程正在运行"+i);
}
}
}
4.3 线程插队:
join():
- 在某个线程中调用其他线程的join()方法,调用的线程将被阻塞,直到join()方法加入的线程执行完它才会执行
- 需要抛出或处理异常InterruptedException
package ThreadMethodTest;
public class JoinTest implements Runnable {
@Override
public void run() {
for (int i = 0; i < 100; i++) {
//Thread.currentThread().getName()获取当前执行线程的名字
System.out.println(Thread.currentThread().getName()+"正在执行中"+i);
if(i==50){
}
}
}
public static void main(String[] args) throws InterruptedException {
Thread thread1 = new Thread(new JoinTest());
thread1.start();
for (int i = 0; i < 100; i++) {
if(i==50){
thread1.join();
}
System.out.println("main线程执行"+i);
}
}
}
五、多线程同步:
多线程可以提高程序的效率,但是也会引发一些安全问题。例如:售票时,如果多个线程同时取同一张票,就可能导致错误。
引发错误的代码:
package synchronizedTest;
public class ErrorTest implements Runnable{
private int tickets = 10;
@Override
public void run() {
try {
while (tickets>0){
Thread.sleep(100);
System.out.println(Thread.currentThread().getName()+"当前售出第"+(tickets--)+"张票");
}
}catch (Exception e){
e.printStackTrace();
}
}
public static void main(String[] args) {
ErrorTest errorTest = new ErrorTest();
Thread thread1 = new Thread(errorTest,"售票员1");
Thread thread2 = new Thread(errorTest,"售票员2");
Thread thread3 = new Thread(errorTest,"售票员3");
thread1.start();
thread2.start();
thread3.start();
}
}
可以看出一张票可能被售出多次,甚至可能会出现票为负数的情况。这是因为当线程A正在取票,但是票的数量还未减1时,线程B也要取票,这样就导致取出了重复的票,显然这是不正确的,所以我们需要进行同步,来避免问题的出现
5.1 同步代码块:
为保证共享资源在任何时刻都只能有一个线程访问,Java提供了同步机制。当多个线程使用同一个共享资源时,可以将处理共享资源的代码放置在一个代码块中,使用synchronized关键字修饰,称为同步代码块
synchronized(lock){
//操纵共享资源的代码块
}
lock是一个锁对象,它是同步代码块的关键。默认情况下lock为1,表示可以访问。如果当前有线程正在访问共享资源,则lock为0。不允许新的线程访问共享资源,使新线程进入阻塞状态。只有正在访问的线程离开后lock会重新置为1,允许访问。
对上面的错误代码进行修改:
package synchronizedTest;
public class ErrorTest_yes implements Runnable{
private int tickets = 10;
private Object lock = new Object();
@Override
public void run() {
synchronized (lock){
try {
while (tickets>0) {
Thread.sleep(100);
System.out.println(Thread.currentThread().getName()
+ "当前售出第" + (tickets--) + "张票");
}
}catch (Exception e){
e.printStackTrace();
}
}
}
public static void main(String[] args) {
ErrorTest_yes yes = new ErrorTest_yes();
Thread thread1 = new Thread(yes,"售票员A");
Thread thread2 = new Thread(yes,"售票员B");
Thread thread3 = new Thread(yes,"售票员C");
thread1.start();
thread2.start();
thread3.start();
}
}
tip:同步代码块中lock对象可以是任意类型的对象,但是多个线程共享的对象必须是唯一的。lock对象的创建不能放在run方法中,这样的话每一个线程都会拥有一个自己的锁,无法起到同步作用。
5.2 同步方法:
被synchronized修饰的方法就是同步方法,可以实现与同步代码块相同的功能
//synchronized 返回值 方法名([参数1,...]){}
使用同步方法修改上面的错误代码:
package synchronizedTest;
public class ErrorTest_yes implements Runnable{
private int tickets = 10;
private Object lock = new Object();
@Override
public void run() {
saleTicket();
}
private synchronized void saleTicket(){
try {
while (tickets>0) {
Thread.sleep(100);
System.out.println(Thread.currentThread().getName()
+ "当前售出第" + (tickets--) + "张票");
}
}catch (Exception e){
e.printStackTrace();
}
}
public static void main(String[] args) {
ErrorTest_yes yes = new ErrorTest_yes();
Thread thread1 = new Thread(yes,"售票员A");
Thread thread2 = new Thread(yes,"售票员B");
Thread thread3 = new Thread(yes,"售票员C");
thread1.start();
thread2.start();
thread3.start();
}
}
- 通过上面的代码大家可以看出也实现了同步。但是同步方法没有传入lock对象啊,他是怎么进行同步的呢?
答:其实同步方法的锁就是this对象。例如上代码,因为同步方法是被线程共享的,所以所有的线程都使用同一个yes对象,自然也就可以使用this来保证同步效果
- 那么问题又来了?如果我们**用静态同步方法呢?**这时候是没有this的,他又是如何同步的呢?
答:静态同步方法的锁是静态方法所在的类的Class对象。因为在Java类加载机制中,类只被创建一次。所以也就可以被用来作为锁对象了
5.3 死锁问题:
有这样一个场景:一个中国人和一个美国人在一起吃饭,美国人拿了中国人的筷子,
中国人拿了美国人的刀叉,两个人开始争执不休: .
中国人:“你先给我筷子,我再给你刀叉!”
美国人:“你先给我刀叉,我再给你筷子!”
…
结果可想而知:两个人都吃不到饭。类似的问题还有哲学家就餐问题。有兴趣大家可以自行了解。此处不做赘述
代码模拟死锁问题:
package synchronizedTest;
public class DeadLock implements Runnable {
private static Object chopsticks = new Object(); //筷子的锁
private static Object knifeAndFork = new Object(); //刀叉的锁
private boolean flag; //flag带表是美国人还是中国人
public DeadLock(boolean flag){
this.flag = flag;
}
@Override
public void run() {
if(flag){ //当前说老美
//筷子锁对象上的同步代码块
synchronized (chopsticks){
System.out.println("把叉子给我,我就把筷子给你");
try {
Thread.sleep(1000);
} catch (InterruptedException e) {
e.printStackTrace();
}
synchronized (knifeAndFork){ //开始伸手要刀叉
System.out.println("双标老美拿到刀叉");
}
}
}else{
//刀叉对象的同步代码块
synchronized (knifeAndFork) {
System.out.println("把筷子给我,我就把叉子给你");
try {
Thread.sleep(1000);
} catch (InterruptedException e) {
e.printStackTrace();
}
synchronized (chopsticks) {
System.out.println("伟大的中国人民拿到筷子");
}
}
}
}
public static void main(String[] args) {
DeadLock American = new DeadLock(true);
DeadLock Chinese = new DeadLock(false);
//创建并开启两个线程
new Thread(American,"双标美").start();
new Thread(Chinese,"博爱中").start();
}
}
由上面代码可以看出双方互不松手,程序陷入死锁。所以在编程中我们需要避免死锁问题的发生
5.6 多线程通信
经典例子:生产者和消费者问题。
假设有一个场景:有一个仓库,生产者往里面放货物,消费者从里面取货物。如果仓库满了,如何让生产者停下后通知消费者取货。如果仓库空了,如何停止取货,让消费者通知生产者生产?
代码模拟一下:
package communicationTest;
/** * 定义一个仓库类 */
public class Storage {
//数据存储数组
private int[] cells = new int[10];
//inPos表示存入时数组下标,outPos表示取出时数组下标
private int inPos;
private int outPos;
public void put(int num){
cells[inPos] = num;
System.out.println("在cells["+inPos+"]中放入数据--"+cells[inPos]);
inPos++;
//每当数据已经放满就从0位置重新开始放数据
if(inPos == cells.length){
inPos=0; //当inPos为数组长度时,将其置为0
}
}
//定义一个get方法从数组中取出数据
public void get(){
int data = cells[outPos];
System.out.println("在cells["+outPos+"]中取出数据--"+cells[outPos]);
outPos++; //取完让元素位置++
//每当数据已经取完就从0位置重新开始取数据
if(outPos==cells.length){
outPos=0;
}
}
}
生产者和消费者类:
package communicationTest;
/** * 生产者和消费者类 * 生产者不断生产 * 消费者不断消费 */
class Input implements Runnable {
private Storage st;
private int flag=100;
private int num;
Input(Storage st){
this.st = st;
}
@Override
public void run() {
while ((flag--)>0){
st.put(num++);
}
}
}
class Output implements Runnable {
private Storage st;
private int flag=100;
Output(Storage st){
this.st = st;
}
@Override
public void run() {
while ((flag--)>0){
st.get();
}
}
}
public class InputAndOutput{
public static void main(String[] args) {
//创建一个仓库对象
Storage st = new Storage();
Input input = new Input(st);
Output output = new Output(st);
new Thread(input).start();
new Thread(output).start();
}
}
根据运行结果能够发现,已经被放过数据还未被取出的位置又被重复放上数据,这是错误的。
那么如何解决问题呢?
此时就需要让线程之间彼此通信。Object类中提供了wait()、notify()、notifyAll()方法用于解决线程间的通信问题
- wait():使当前线程放弃同步锁并进入等待,直到其他线程进入此同步锁,并调用notify()方法,或notifyAll()方法唤醒该线程为止
- notify():唤醒此同步锁上等待的第一个调用wait()方法的线程
- notifyAll():唤醒此同步锁上调用wait()方法的所有线程
注意:以上三个方法的调用者都应该是同步锁对象,如果不是则会抛出异常
对上面Storage代码的修改
package communicationTest;
/** * 定义一个仓库类 */
public class Storage {
//数据存储数组
private int[] cells = new int[10];
//inPos表示存入时数组下标,outPos表示取出时数组下标
private int inPos;
private int outPos;
private int count; //存入或取出数据的数量
public synchronized void put(int num) {
if(count==cells.length){
try {
this.wait();
} catch (InterruptedException e) {
e.printStackTrace();
}
}
cells[inPos] = num;
System.out.println("在cells["+inPos+"]中放入数据--"+cells[inPos]);
inPos++;
count++; //放入一个元素count++
//如果已经放到最后一个位置,则从头开始放。(模拟循环队列)
if (inPos==cells.length){
inPos=0;
}
this.notify();
}
//定义一个get方法从数组中取出数据
public synchronized void get() {
if(count==0){ //如果已经全部取出
try {
this.wait();
} catch (InterruptedException e) {
e.printStackTrace();
}
}
int data = cells[outPos];
System.out.println("在cells["+outPos+"]中取出数据--"+data);
cells[outPos]=-1; //代表此处无元素
outPos++; //取完让元素位置++
count--;
if(outPos==cells.length){
outPos=0;
}
this.notify(); //表示仓库已经可以放货物,通知生产者生产
}
}
此时便不会出现重复放入元素或重复取出元素的情况