目录
一、多线程概述
线程是操作系统能够进行运算调度的最小单位,
它被包含在进程之中,是进程中的实际运作单位,
一条线程指的是进程中一个单一顺序的控制流,一个进程中可以并发多个线程,每条线程并行执行不同的任务,
而多线程则是并发执行程序,提高程序的效率,达到同时完成多项工作的目的。
二、Java程序运行原理
java命令会启动java虚拟机JVM,等于启动了一个应用程序,也就是启动了一个进程。
该进程会自动启动一个“主线程”,然后主线程去带调用某个类中的main方法。
注意:JVM的启动至少启动了垃圾回收线程和主线程,所以说JVM启动是多线程的。
三、多线程实现
3.1继承Thread类
我们可以定义自己的类,继承Thread,并且重写run方法,
然后把新线程要做的事情写在run方法中,然后创建线程对象,调用start方法开启新线程,内部会自动执行run方法。
public class ThreadTest {
public static void main(String[] args) {
myThread mt=new myThread();//创建线程子类对象
mt.start();//创建并启动子线程,执行run方法
for (int i = 0; i < 1000; i++) {
System.out.println("run main method!");
}
}
}
class myThread extends Thread{
@Override
public void run() {
for (int i = 0; i < 1000; i++) {
System.out.println("run thread method!");
}
}
}
可以看到主方法线程和子线程的语句交替执行输出语句,
3.2实现Runnable接口
我们还可以通过实现Runnable接口的方法来完成多线程,并重写其中的run方法,
把新线程要做的事写在run方法中,创建Runnable的子类对象,然后创建Thread对象,传入Runnable子类对象,
接着调用start方法开启线程,内部会自动调用Runnable子类对象的run方法,
public class ThreadTest {
public static void main(String[] args) {
myRunnable mr=new myRunnable();
Thread t=new Thread(mr);
t.start();
for (int i = 0; i < 1000; i++) {
System.out.println("main method");
}
}
}
class myRunnable implements Runnable{
@Override
public void run() {
for (int i = 0; i < 1000; i++) {
System.out.println("Runnable method");
}
}
}
我们可以看到两个线程并发执行,交替输出,
3.3两种方式的区别
源码区别:
- 继承Thread:由于子类重写了Thread类的run()方法,当调用start()方法时,直接找子类的run()方法执行
- 实现Runnable:通过构造函数传入Runnable的引用,并将该引用赋值给了成员变量,start()方法调用run()方法时内部会判断成员变量的Runnable引用是否为空,不为空那么编译的时候看Runnable的run()方法,运行时执行的是Runnable子类的run()方法
一般区别:
- 继承Thread
- 优点:可以直接使用Thread类中的方法,代码简单
- 缺点:如果已经有了其他的父类,那么就不可以使用这种方法(java中仅支持单继承)
- 实现Runnable
- 优点:即使自定义的线程类有了别的父类,也可以实现Runnable接口(java中接口是支持多实现的)
- 缺点:不可以直接使用Thread中的方法,需要先获取到线程对象后才可以得到Thread的方法,代码复杂
3.4匿名内部类实现线程
使用匿名内部类就不用自定义一个线程类,在主方法中可以直接使用,代码整体感好。
我们来看看这两种方式如何用匿名内部类实现。
3.4.1继承Thread
public class ThreadTest {
public static void main(String[] args) {
new Thread(){
@Override
public void run() {
for (int i = 0; i < 1000; i++) {
System.out.println("thread method");
}
}
}.start();
for (int i = 0; i < 1000; i++) {
System.out.println("main method");
}
}
}
3.4.2实现Runnable
public class ThreadTest {
public static void main(String[] args) {
new Thread(new Runnable() {
@Override
public void run() {
for (int i = 0; i < 1000; i++) {
System.out.println("runnable method");
}
}
}).start();
for (int i = 0; i < 1000; i++) {
System.out.println("main method");
}
}
}
四、线程的方法
4.1获取与设置名字
我们可以通过getName和setName获取和设置线程的名字,还可以直接使用构造方法传入字符串设置名字,
如果不设置线程的名字,线程的默认名字是Thread-x,其中x为编号,默认从0开始,按照创建的顺序以此类推,
public class ThreadTest {
public static void main(String[] args) {
new Thread(){//不认为设置名字,使用默认名字
@Override
public void run() {
System.out.println(getName());
}
}.start();
new Thread("thread1"){//通过构造方法设置名字
@Override
public void run() {
System.out.println(getName());
}
}.start();
new Thread(){//通过setName方法设置名字
@Override
public void run() {
this.setName("thread2");
System.out.println(getName());
}
}.start();
}
}
4.2线程的休眠
我们还可以使用静态方法sleep来控制线程暂停多少毫秒,使用方法为Thread.sleep(毫秒值),
public class ThreadTest {
public static void main(String[] args) {
new Thread("thread1"){
@Override
public void run() {
for (int i = 0; i < 3; i++) {
System.out.println(getName());
try {
Thread.sleep(1000);
} catch (InterruptedException e) {
e.printStackTrace();
}
}
}
}.start();
new Thread("thread2"){
@Override
public void run() {
for (int i = 0; i < 3; i++) {
System.out.println(getName());
try {
Thread.sleep(1000);
} catch (InterruptedException e) {
e.printStackTrace();
}
}
}
}.start();
}
}
我们可以看到线程1执行一次后就暂停休息1s钟,然后程序会执行线程2,线程2执行完一次后也休息了1s钟,如此反复交替输出,
4.3守护线程setDaemon()
守护线程就是当程序没有其他线程时,守护线程会立即退出,即使没有执行完,我们可以通过setDaemon()的方法设置线程为守护线程,
守护线程不会单独执行,如果有其他线程时则并发执行,如果其他线程都执行完了,守护线程也会立马退出不会继续执行,
我们设置一下线程1为守护线程,输出名字100次,线程2为普通线程,输出名字5次,让线程2先启动,然后看看线程1会输出几次名字,
public class ThreadTest {
public static void main(String[] args) {
Thread t1=new Thread("thread1"){
@Override
public void run() {
for (int i = 0; i < 100; i++) {
System.out.println(getName());
}
}
};
Thread t2=new Thread("thread2"){
@Override
public void run() {
for (int i = 0; i < 3; i++) {
System.out.println(getName());
}
}
};
t1.setDaemon(true);//设置线程1为守护线程
t2.start();
t1.start();
}
}
我们可以看到当线程2执行完毕之后,线程1还是输出了几条语句,但是远没有100条,
看来即使设置了守护线程,也不会一点都不运行了,在退出的时间里,守护线程还是执行了一小段时间的,
4.4加入线程join()
使用join方法可以使当前线程暂停,等待调用join方法的线程执行结束之后,当前线程继续执行,
还可以给join方法传入int类型的毫秒值,指定等待的时间,我们看看如何具体使用join()方法,
public class ThreadTest {
public static void main(String[] args) {
Thread t1=new Thread("thread1"){
@Override
public void run() {
for (int i = 0; i < 3; i++) {
System.out.println(getName());
}
}
};
Thread t2=new Thread("thread2"){
@Override
public void run() {
for (int i = 0; i < 3; i++) {
if(i==1){//当线程输出一次后,让t1线程加入,暂停t2线程
try {
t1.join();
} catch (InterruptedException e) {
e.printStackTrace();
}
}
System.out.println(getName());
}
}
};
t2.start();//让t2线程先执行
t1.start();
}
}
可以看到t2线程输出一次后,让t1线程加入,暂停了t2线程,执行完了t1线程之后才继续输出的t2,
4.5线程的优先级
线程的优先级即线程优先执行的等级,在java线程中,优先级范围为1-10,
数字越大代表优先级越高,程序也就越有可能更多的占用CPU时间,当然也不是完全不允许执行别的线程,
优先级高代表更有可能执行该线程,我们通过setPriority()方法来设置线程的优先级,下面我们看看具体如何使用,
public class ThreadTest {
public static void main(String[] args) {
Thread t1=new Thread("thread1"){
@Override
public void run() {
for (int i = 0; i < 50; i++) {
System.out.println(getName());
}
}
};
Thread t2=new Thread("thread2"){
@Override
public void run() {
for (int i = 0; i < 50; i++) {
System.out.println(getName());
}
}
};
t1.setPriority(1);//设置t1优先级为1
t2.setPriority(10);//设置t2优先级为10
t1.start();
t2.start();
}
}
可以看到虽然是t1线程先启动,但是t2的线程优先级比较高,所以程序一开始让t2执行,
五、同步
5.1同步代码块
当多线程并发时,有多段代码同时执行,我们希望执行某一段代码过程中不要切换到其他的线程工作,这个时候就需要同步,
如果两段代码是同步的,那么同一时间只能执行一段,在一段代码没执行结束之前,不会执行另外一段代码,这就叫做同步。
在java中,我们使用关键字synchronized加上一个锁对象来定义一段代码,这样的代码我们称同步代码块,
其中锁对象可以是任意的对象,但是不可以是匿名对象,
如果希望多段代码是同步的,那么他们的锁对象必须相同,我们举个例子看看如何使用synchronized关键字,
首先我们不用同步,看看输出这么一个程序会有什么结果,
public class ThreadTest {
public static void main(String[] args) {
Printer p=new Printer();
new Thread(){
@Override
public void run() {
for (int i = 0; i < 100; i++) {
p.print1();
}
}
}.start();
new Thread(){
@Override
public void run() {
for (int i = 0; i < 100; i++) {
p.print2();
}
}
}.start();
}
}
class Printer{
public void print1(){
System.out.print("j");
System.out.print("a");
System.out.print("v");
System.out.print("a");
System.out.print("\r\n");
}
public void print2(){
System.out.print("c");
System.out.print("+");
System.out.print("+");
System.out.print("\r\n");
}
}
结果可以看到有些字符输出的不完整或者是拼接的错误字符,
很明显是线程输出执行到一半切换到了别的线程导致的 ,这种问题我们就可以通过加入关键字synchronized来定义同步代码块解决,
class Printer{
public void print1(){
synchronized (this) {
System.out.print("j");
System.out.print("a");
System.out.print("v");
System.out.print("a");
System.out.print("\r\n");
}
}
public void print2(){
synchronized (this) {
System.out.print("c");
System.out.print("+");
System.out.print("+");
System.out.print("\r\n");
}
}
}
这里我们用this本对象为锁,将两个方法变成了同步代码块,输出的时候就没有乱序的情况出现了。
5.2同步方法
我们还可以使用synchronized关键字修饰一个方法,该方法中所有的代码都是同步的,
同步方法只需要在方法上加synchronized关键字即可,而锁对象有两种,
非静态的同步方法锁对象为this对象,静态的同步方法锁对象为该类的字节码对象(例Printer.class)。
class Printer{
public synchronized void print1(){
System.out.print("j");
System.out.print("a");
System.out.print("v");
System.out.print("a");
System.out.print("\r\n");
}
public synchronized void print2(){
System.out.print("c");
System.out.print("+");
System.out.print("+");
System.out.print("\r\n");
}
}
5.3线程安全问题
当多线程并发操作同一个数据时,就有可能出现线程安全问题(比如数据的更改覆盖等),使用同步就可以解决这个问题,
把操作数据的代码进行同步,不允许多个线程一起操作,我们以卖火车票为例子来看看如何使用同步解决这个问题,
现在有100张火车票,通过四个窗口进行销售,请用线程模拟窗口进行火车票售票,
5.3.1继承Thread
我们先使用继承Thread类的方法来写一下代码,
public class ThreadTest {
public static void main(String[] args) {
//创建四个线程模拟窗口进行售票
new Ticket().start();
new Ticket().start();
new Ticket().start();
new Ticket().start();
}
}
class Ticket extends Thread{
private static int ticket=100;//火车票余量,必须用静态修饰,否则四个窗口剩余票数不共享
@Override
public void run() {
while(true){
if(ticket<=0){
break;
}
//使用线程睡眠10ms模拟卖票的时间消耗
try {
Thread.sleep(10);
} catch (InterruptedException e) {
e.printStackTrace();
}
System.out.println(getName() + "还有" + --ticket + "张票");
}
}
}
我们可以看到已经卖到负数了,很明显这是不符合逻辑的,
这是因为多个线程在短时间内同时对ticket余量进行了操作,这样直接访问票数是不安全的, 我们需要在该代码上加入同步,
class Ticket extends Thread{
private static int ticket=100;//火车票余量,必须用静态修饰,否则四个窗口剩余票数不共享
@Override
public void run() {
while(true){
//注意这里的锁对象不可以使用this,因为主程序中创建了四个对象,也就有四个this
//所以这里我们使用本类的字节码对象,也可以使用在Ticket类中声明的静态对象
synchronized (Ticket.class) {
if (ticket <= 0) {
break;
}
//使用线程睡眠10ms模拟卖票的时间消耗
try {
Thread.sleep(10);
} catch (InterruptedException e) {
e.printStackTrace();
}
System.out.println(getName() + "还有" + --ticket + "张票");
}
}
}
}
然后我们会发现没有负数票,安全问题完美解决,
5.3.2实现Runnable接口
实现Runnable接口方法类似,其主要代码如下,
public class ThreadTest {
public static void main(String[] args) {
//创建四个线程模拟窗口进行售票
Ticket t=new Ticket();
new Thread(t).start();
new Thread(t).start();
new Thread(t).start();
new Thread(t).start();
}
}
class Ticket implements Runnable{
private int ticket=100;//我们可以只创建一个对象,所以不用静态修饰
@Override
public void run() {
while(true){
//因为只创建一个对象,所以也可以直接使用this为锁对象
synchronized (this) {
if (ticket <= 0) {
break;
}
//使用线程睡眠10ms模拟卖票的时间消耗
try {
Thread.sleep(10);
} catch (InterruptedException e) {
e.printStackTrace();
}
System.out.println(Thread.currentThread().getName() + "还有" + --ticket + "张票");
}
}
}
}
5.4死锁问题
多线程同步的时候,如果同步代码嵌套,使用了相同锁对象,就有可能出现死锁,
我们现在给两个人一双筷子,一个人给左筷子,一个人给右筷子,如果想要吃饭则必须说服另一个人把筷子给他然后吃饭,
现在我们用线程模拟人,线程0分配了左筷子,线程1分配了右筷子,然后启动这两个线程看看程序会怎样运行,
public class ThreadTest {
private static String s1="左筷子";
private static String s2="右筷子";
public static void main(String[] args) {
new Thread(){
@Override
public void run() {
synchronized (s1){
while(true){
System.out.println(getName()+"已有"+s1+"等待"+s2);
synchronized (s2){
System.out.println(getName()+"拿到"+s2+"开始吃饭");
}
}
}
}
}.start();
new Thread(){
@Override
public void run() {
synchronized (s2){
while(true){
System.out.println(getName()+"已有"+s2+"等待"+s1);
synchronized (s1){
System.out.println(getName()+"拿到"+s1+"开始吃饭");
}
}
}
}
}.start();
}
}
class Ticket implements Runnable{
private int ticket=100;//我们可以只创建一个对象,所以不用静态修饰
@Override
public void run() {
while(true){
//因为只创建一个对象,所以也可以直接使用this为锁对象
synchronized (this) {
if (ticket <= 0) {
break;
}
//使用线程睡眠10ms模拟卖票的时间消耗
try {
Thread.sleep(10);
} catch (InterruptedException e) {
e.printStackTrace();
}
System.out.println(Thread.currentThread().getName() + "还有" + --ticket + "张票");
}
}
}
}
我们可以看到程序执行到一半不动卡住了,当线程1开始执行的时候,线程0想要右筷子,线程1想要左筷子,
而线程0想要的右筷子在线程1手上,线程1想要的左筷子在线程0手上,两个线程面对资源都不让步,最终导致了死锁程序卡住,
所以为了避免同步代码块出现死锁的情况,请尽量不要使用同步的嵌套!
六、单例设计模式
6.1单例设计模式概述与实现
单例设计模式即保证类在内存中只有一个对象,主要有以下三种方法实现:
1、饿汉式(开发常用)
- 将构造方法私有化,让其他类无法通过构造方法获取该类的对象
- 然后自己创建私有静态本类对象(直接创建对象)
- 通过get方法返回该对象
class Single{
private Single(){}
private static Single s=new Single();
public static Single getSingle(){
return s;
}
}
2、懒汉式(又称单例延迟加载模式,面试常用)
- 将构造方法私有化,让其他类无法通过构造方法获取该类的对象
- 然后声明一个本类的引用(使用的时候再创建对象)
- 提供一个公共的访问方法
class Single{
private Single(){}
private static Single s;
public static Single getSingle(){
if(s==null){
s=new Single();
}
return s;
}
}
该方法在多线程访问的时候,有可能会有安全隐患,
因为如果在判断s==null条件满足后,如果切换到别的线程,那么下一个线程该条件也满足,就会创建多个对象,就不是单例模式了。
3、final修饰
- 将构造方法私有化,让其他类无法通过构造方法获取该类的对象
- 自己创建公有静态final本类对象,使用final修饰之后就不允许被更改了
class Single{
private Single(){}
public static final Single s=new Single();
}
6.2 Runtime类
Runtime类就是一个单例类,该类构造方法被私有化,
并且定义了一个私有的对象,通过getRuntime获取到该对象,
import java.io.IOException;
public class ThreadTest {
public static void main(String[] args) throws IOException {
Runtime r=Runtime.getRuntime();//通过静态方法获取对象
r.exec("shutdown -s -t 600");//执行字符串命令,类似cmd命令行命令,这里我们演示关机命令
//r.exec("shutdown -a");//取消关机命令
}
}
七、Timer类
Timer即计时器,是一种工具,用于线程安排在后台执行的任务,
可以安排任务执行一次,或者定期重复执行,它有一个schedule()方法用于安排任务,
void schedule(TimerTask task, Date time)//安排在指定的时间执行指定的任务
void schedule(TimerTask task, Date firsttime, long period)//安排指定的任务在指定时间开始后,每隔一段时间重复执行一次,period的单位为毫秒
其中TimerTask就是任务,实现了Runnable接口,故任务需要在run方法里面重写,我们来看看具体如何使用该方法,
import java.util.Date;
import java.util.Timer;
import java.util.TimerTask;
public class ThreadTest {
public static void main(String[] args) throws InterruptedException {
Timer t=new Timer();
t.schedule(new myTimerTask(),new Date(121,0,29,19,50),3000);//从指定时间开始,每隔3s重复执行一次
//每隔1s输出当前时间
while(true){
Thread.sleep(1000);
System.out.println(new Date());
}
}
}
class myTimerTask extends TimerTask{
@Override
public void run() {
System.out.println("学习java");
}
}
我们可以看到到了我们预设的时间时,程序就执行了run方法,开始学习了java,
八、线程之间的通信
当多个线程并发执行时,在默认情况下CPU是随机切换现成的,
如果我们希望线程之间有规律的执行,就可以使用通信,例如每个线程执行一次打印。
8.1两个线程之间的通信
两个线程之间的通信可以通过以下方法实现,
- 如果希望线程等待,那么就调用wait()方法
- 如果希望唤醒等待的线程,那么就调用notify()方法
- 这两种方法必须在同步代码块中执行,并且使用同步锁对象来调用
我们来看看具体如何实现,
import java.io.PrintWriter;
public class ThreadTest {
public static void main(String[] args){
Printer p=new Printer();
new Thread(){
@Override
public void run() {
for (int i = 0; i < 5; i++) {
try {
p.print1();
} catch (InterruptedException e) {
e.printStackTrace();
}
}
}
}.start();
new Thread(){
@Override
public void run() {
for (int i = 0; i < 5; i++) {
try {
p.print2();
} catch (InterruptedException e) {
e.printStackTrace();
}
}
}
}.start();
}
}
class Printer{
private int flag=1;
public void print1() throws InterruptedException {
synchronized (this) {
if(flag!=1){
this.wait();//执行一次后让当前进程等待
}
System.out.print("j");
System.out.print("a");
System.out.print("v");
System.out.print("a");
System.out.print("\r\n");
flag=2;
this.notify();//随机唤醒单个等待的线程
}
}
public void print2() throws InterruptedException {
synchronized (this) {
if(flag!=2){
this.wait();//执行一次后让当前进程等待
}
System.out.print("c");
System.out.print("+");
System.out.print("+");
System.out.print("\r\n");
flag=1;
this.notify();//随机唤醒单个等待的线程
}
}
}
可以看到内容交替输出了,
8.2多个线程之间的通信
多个线程之间的通信可以通过以下方法实现,
- 如果希望线程等待,那么就调用wait()方法
- 如果希望唤醒等待的线程,那么就调用notifyAll()方法
- 这两种方法必须在同步代码块中执行,并且使用同步锁对象来调用
多个线程之间的通信和两个之间的通信在唤醒上有不同,多个使用的是notifyAll()的方法,唤醒全部的等待线程
而两个之间的通信使用的是notify(),随机唤醒一个等待的线程,下面我们看看具体如何实现多个线程的通信,
import java.io.PrintWriter;
public class ThreadTest {
public static void main(String[] args){
Printer p=new Printer();
new Thread(){
@Override
public void run() {
for (int i = 0; i < 3; i++) {
try {
p.print1();
} catch (InterruptedException e) {
e.printStackTrace();
}
}
}
}.start();
new Thread(){
@Override
public void run() {
for (int i = 0; i < 3; i++) {
try {
p.print2();
} catch (InterruptedException e) {
e.printStackTrace();
}
}
}
}.start();
new Thread(){
@Override
public void run() {
for (int i = 0; i < 3; i++) {
try {
p.print3();
} catch (InterruptedException e) {
e.printStackTrace();
}
}
}
}.start();
}
}
class Printer{
private int flag=1;
public void print1() throws InterruptedException {
synchronized (this) {
while(flag!=1){
this.wait();//执行一次后让当前进程等待
}
System.out.print("j");
System.out.print("a");
System.out.print("v");
System.out.print("a");
System.out.print("\r\n");
flag=2;
this.notifyAll();//唤醒所有等待的线程
}
}
public void print2() throws InterruptedException {
synchronized (this) {
while(flag!=2){
this.wait();//执行一次后让当前进程等待
}
System.out.print("c");
System.out.print("+");
System.out.print("+");
System.out.print("\r\n");
flag=3;
this.notifyAll();//唤醒所有等待的线程
}
}
public void print3() throws InterruptedException {
synchronized (this) {
while(flag!=3){
this.wait();//执行一次后让当前进程等待
}
System.out.print("p");
System.out.print("y");
System.out.print("t");
System.out.print("o");
System.out.print("n");
System.out.print("\r\n");
flag=1;
this.notifyAll();//唤醒所有等待的线程
}
}
}
我们可以看到三个线程交替输出了结果,
8.3线程间通信注意问题
- 在同步代码块中,用的什么锁对象,就用什么对象调用wait()和notify()方法
- 为什么wait()和notify()方法定义在了Object类中?
- 因为锁对象可以是任意对象,Object类是所有对象的基类,所以wait()和notify()方法定义在了Object类中
- sleep()方法和wait()方法的区别
- sleep()方法必须传入参数,即睡眠的毫秒值,时间到了自动醒来;在同步中不释放锁,不允许其他线程执行
- wait()方法可以传入参数,也可以不传,传入就是在参数时间结束后等待,不传入则是直接等待,都不会醒来;在同步中释放锁,可以允许其他线程执行
8.4互斥锁
在JDK1.5版本之后,我们可以使用ReentrantLock类的lock()和unlock()方法来进行同步(不使用synchronized关键字),
还可以实现线程的通信,首先使用ReentrantLock类的newCondition()方法获取Condition对象,
需要等待的时候使用Condition对象的await()方法,唤醒的时候用signal()方法,
不同的线程使用不同的Condition对象,这样就可以唤醒指定的线程了,我们用互斥锁实现一下前面的例子,
import java.io.PrintWriter;
import java.util.concurrent.locks.Condition;
import java.util.concurrent.locks.ReentrantLock;
public class ThreadTest {
public static void main(String[] args){
Printer p=new Printer();
new Thread(){
@Override
public void run() {
for (int i = 0; i < 3; i++) {
try {
p.print1();
} catch (InterruptedException e) {
e.printStackTrace();
}
}
}
}.start();
new Thread(){
@Override
public void run() {
for (int i = 0; i < 3; i++) {
try {
p.print2();
} catch (InterruptedException e) {
e.printStackTrace();
}
}
}
}.start();
new Thread(){
@Override
public void run() {
for (int i = 0; i < 3; i++) {
try {
p.print3();
} catch (InterruptedException e) {
e.printStackTrace();
}
}
}
}.start();
}
}
class Printer{
private ReentrantLock r=new ReentrantLock();
private Condition c1=r.newCondition();
private Condition c2=r.newCondition();
private Condition c3=r.newCondition();
private int flag=1;
public void print1() throws InterruptedException {
r.lock();//获取锁
if(flag!=1){
c1.await();//c1等待
}
System.out.print("j");
System.out.print("a");
System.out.print("v");
System.out.print("a");
System.out.print("\r\n");
flag=2;
c2.signal();//唤醒c2
r.unlock();//释放锁
}
public void print2() throws InterruptedException {
r.lock();//获取锁
if(flag!=2){
c2.await();//c2等待
}
System.out.print("c");
System.out.print("+");
System.out.print("+");
System.out.print("\r\n");
flag=3;
c3.signal();//唤醒c3
r.unlock();//释放锁
}
public void print3() throws InterruptedException {
r.lock();//获取锁
if(flag!=3){
c3.await();//c3等待
}
System.out.print("p");
System.out.print("y");
System.out.print("t");
System.out.print("o");
System.out.print("n");
System.out.print("\r\n");
flag=1;
c1.signal();//唤醒c1
r.unlock();//释放锁
}
}
可以看到效果是一样的,
九、线程组
在java中我们使用ThreadGroup来表示线程组,它可以对一批线程进行分类管理,
java允许程序直接对线程组进行控制,在默认情况下,所有线程都属于主线程组,
public final ThreadGroup getThreadGroup()//通过线程对象获取它所属于的组
public final String getName()//通过线程组对象获取它组的名字
我们还可以给线程设置分组,步骤如下:
- ThreadGroup(String name) 创建线程组对象并给其赋值名字
- 创建线程对象
- Thread(ThreadGroup group, Runnable target, String name) 创建线程并分配到指定线程组中
- 设置线程组的优先级或者守护线程
下面我们看看具体如何使用线程组,
public class ThreadTest {
public static void main(String[] args){
ThreadGroup tg=new ThreadGroup("TG");//创建线程组TG
myRunnable mr=new myRunnable();
Thread t1=new Thread(tg,mr,"Thread-1");//创建线程t1,并分配到线程组TG中
Thread t2=new Thread(tg,mr,"Thread-2");//创建线程t2,并分配到线程组TG中
System.out.println(t1.getThreadGroup().getName());//输出线程t1所在线程组名字
System.out.println(t2.getThreadGroup().getName());//输出线程t2所在线程组名字
}
}
class myRunnable implements Runnable{
@Override
public void run() {
System.out.println(Thread.currentThread().getName());
}
}
十、线程的状态
在java中,我们将线程的生命周期分为了五种状态,
1、新建(NEW):新创建了一个线程对象。
2、可运行(RUNNABLE):线程对象创建后,其他线程(比如main线程)调用了该对象的start()方法。该状态的线程位于可运行线程池中,等待被线程调度选中,获取cpu 的使用权 。
3、运行(RUNNING):可运行状态(runnable)的线程获得了cpu 时间片(timeslice) ,执行程序代码。
4、阻塞(BLOCKED):阻塞状态是指线程因为某种原因放弃了cpu 使用权,也即让出了cpu时间片,暂时停止运行。直到线程进入可运行(runnable)状态,才有机会再次获得cpu时间片转到运行(running)状态。阻塞的情况分三种:
- 等待阻塞:运行(running)的线程执行o.wait()方法,JVM会把该线程放入等待队列(waitting queue)中。
- 同步阻塞:运行(running)的线程在获取对象的同步锁时,若该同步锁被别的线程占用,则JVM会把该线程放入锁池(lock pool)中。
- 其他阻塞:运行(running)的线程执行Thread.sleep(long ms)或t.join()方法,或者发出了I/O请求时,JVM会把该线程置为阻塞状态。当sleep()状态超时、join()等待线程终止或者超时、或者I/O处理完毕时,线程重新转入可运行(runnable)状态。
5、死亡(DEAD):线程run()、main() 方法执行结束,或者因异常退出了run()方法,则该线程结束生命周期。死亡的线程不可再次复生。
十一、线程池
11.1线程池概述与使用
程序启动一个新线程成本是比较高的,因为它涉及到要和操作系统进行交互,
而使用线程池可以很好的提高性能,尤其是当程序中要创建大量生存期很短的线程时,更应该考虑使用线程池,
线程池里的每一个线程代码结束后,并不会死亡,而是再次回到线程池中成为空闲状态,等待下一个对象来使用,
在JDK5之前我们必须手动实现自己的线程池,从JDK5开始,Java内置支持线程池,通过Executors工厂类来产生线程池,
public static ExecutorService newFixedThreadPool(int nThreads)//创建指定线程数的线程池
public static ExecutorService newSingleThreadExecutor()//创建单线程的线程池
它们的返回对象都是ExecutorService线程池服务器,然后我们使用ExecutorService类的submit()方法放入线程,
注意submit()方法可以接收Runnable对象或者Callable对象代表的线程,最后使用shutdown()方法即可关闭线程池,否则程序会一直运行,
import java.util.concurrent.ExecutorService;
import java.util.concurrent.Executors;
public class ThreadTest {
public static void main(String[] args){
ExecutorService pool= Executors.newFixedThreadPool(2);//创建2个线程的线程池
pool.submit(new myRunnable());//将线程放入线程池中
pool.submit(new myRunnable());
pool.shutdown();//关闭线程池
}
}
class myRunnable implements Runnable{
@Override
public void run() {
System.out.println(Thread.currentThread().getName());
}
}
11.2 Callable
上面我们提到线程池服务器的submit()方法是用来将线程添加到线程池的,其中接收的参数有Runnable和Callable,
Callable接口类似于Runnable,二者都是用于创建线程的方法,Callable是除了继承Thread类、实现Runnable接口外第三种创建线程的方式,
但是Runnable不会返回结果,并且无法抛出经过检查的异常,而Callable中有一个call()方法,用来计算结果,
这个计算的结果就是重写的call方法中用户自己定义的计算功能(比如重写call方法实现两个数相加,返回相加的结果),如果无法计算则抛出异常,
import java.util.concurrent.*;
public class ThreadTest {
public static void main(String[] args) throws ExecutionException, InterruptedException {
ExecutorService pool= Executors.newFixedThreadPool(2);//创建2个线程的线程池
Future<Integer> f1=pool.submit(new myCallable(4));//将线程放入线程池中
Future<Integer> f2=pool.submit(new myCallable(100));
System.out.println(f1.get());//输出计算的结果
System.out.println(f2.get());
pool.shutdown();//关闭线程池
}
}
class myCallable implements Callable<Integer>{
private int num;
public myCallable(int num){
this.num=num;
}
@Override//在call函数中实现一个求1到任意数的累加和功能
public Integer call() throws Exception {
int sum=0;
for (int i = 1; i <=num ; i++) {
sum+=i;
}
return sum;
}
}
十二、工厂模式
12.1简单工厂模式概述与使用
简单工厂模式又称为静态工厂方法模式,它定义一个具体的工厂类负责创建一个类的实例,
- 优点:客户端不需要再负责对象的创建,从而明确了各个类的职责
- 缺点:这个静态工厂类负责所有对象的创建,如果有新的对象增加或者某些对象的创建方式不同,则需要不断修改工厂类,后期维护困难
我们看看具体如何使用,
public class ThreadTest {
public static void main(String[] args){
Dog d=(Dog)AnimalFactory.createAnimal("dog");//利用工厂方法创建狗类
d.eat();
Cat c=(Cat)AnimalFactory.createAnimal("cat");//利用工厂方法创建猫类
c.eat();
}
}
abstract class Animal{
public abstract void eat();
}
class Dog extends Animal{
public void eat(){
System.out.println("狗吃骨头");
}
}
class Cat extends Animal{
public void eat(){
System.out.println("猫吃鱼");
}
}
class AnimalFactory{//创建一个动物工厂,用于产生猫和狗对象
public static Animal createAnimal(String name){
if("dog".equals(name)){
return new Dog();
}else if("cat".equals(name)){
return new Cat();
}else{
return null;
}
}
}
12.2工厂方法模式概述与使用
工厂方法模式中抽象工厂类负责定义创建对象的接口,具体对象的创建工作由继承抽象工厂的具体类实现,
换句话说就是先定义一个抽象工厂类,你想创建什么对象就实现什么工厂类,每个类都有对应的工厂类来负责创建对象,
- 优点:客户端不需要再负责对象的创建,从而明确了各个类的职责,如果有新的对象增加,只需要增加一个具体的类和具体的工厂类即可,不影响已有的代码,后期维护容易,增强了系统的扩展性
- 缺点:需要额外的编写代码,增加了工作量
我们看看具体如何使用,
public class ThreadTest {
public static void main(String[] args){
Dog d=(Dog)new DogFactory().createAnimal();//利用狗工厂方法创建狗类
d.eat();
Cat c=(Cat) new CatFactory().createAnimal();//利用猫工厂方法创建猫类
c.eat();
}
}
abstract class Animal{
public abstract void eat();
}
class Dog extends Animal{
public void eat(){
System.out.println("狗吃骨头");
}
}
class Cat extends Animal{
public void eat(){
System.out.println("猫吃鱼");
}
}
interface Factory{//定义抽象工厂类
public Animal createAnimal();
}
class DogFactory implements Factory{//实现狗工厂
public Animal createAnimal() {
return new Dog();
}
}
class CatFactory implements Factory{//实现猫工厂
public Animal createAnimal() {
return new Cat();
}
}