您的位置:首页 > 编程语言 > Java开发

JavaSE---多线程、面向对象设计思想、设计模式

2015-09-18 15:30 579 查看

7.  多线程

7.1 多线程的概述

7.1.1 线程的概念

    什么是线程呢?想要知道线程,就必须先知道进程。进程就是正在运行的程序,是系统进行资源分配和调用的独立单位。每一个进程都有它自己的内存空间和系统资源。

    线程是进程中的单个顺序控制流,是一条执行路径。一个进程如果只有一条执行路径,则称为单线程程序;一个进程如果有多条执行路径,则称为多线程程序。

    一个进程可以有一到多个线程,而一个线程只能属于一个进程。

思考题1:jvm虚拟机的启动是单线程的还是多线程?

    多线程。因为JVM启动时相当于启动了一个进程,然后这个进程会创建一个主线程和垃圾回收线程以及其他的一些线程,主线程会调用main方法,垃圾回收线程会用来回收一些垃圾。

思考题2:并行和并发的区别

    并行是逻辑上同时发生,指在某一个时间内同时运行多个程序。

    并发是物理上同时发生,指在某一个时间点同时运行多个程序。

7.1.2 多线程的利弊

    利处:解决了多部分代码同时运行的问题。

    弊端:线程太多,会导致效率的降低。

7.2 多线程的创建

    由于线程是依赖进程而存在的,所以我们应该先创建一个进程出来,而进程是由系统创建的,所以我们应该去调用系统功能创建一个进程。Java是不能直接调用系统功能的,所以,我们没有办法直接实现多线程程序。但是Java可以去调用C/C++写好的程序来实现多线程程序。由C/C++去调用系统功能创建进程,然后由Java去调用这样的东西,然后提供一些类或者接口供我们使用,这样就可以实现多线程了。创建线程有两种方法:一种是继承Thread类;另一种是通过实现Runnable接口。

7.2.1 继承Thread类

    1)定义一个类并继承Thread。

    2)重写Thread类的 run()方法。 

    3)建立自定义类的一个对象(此时一个线程也被建立)。 

    4)调用线程的start()方法。 

    示例:

<span style="font-size:18px;">public class MyThreadDemo {
public static void main(String[] args) {
//创建两个线程对象
MyThread my1 = new MyThread();
MyThread my2 = new MyThread();

my1.start();
my2.start();
}
}

class MyThread extends Thread {
@Override
public void run() {
// 自己写代码
// System.out.println("好好学习,天天向上");
// 一般来说,被线程执行的代码肯定是比较耗时的,所以我们用循环改进
for(int x = 0;x<100;x++){
System.out.println(x);
}
}
}</span>


    思考1:为什么要重写run()方法呢?

    创建一个线程类,run()方法里面包含的是需要被线程执行的代码。

    思考2:run()和start()的区别?

    run():仅仅是封装被线程执行的代码,直接调用是普通方法

    start():首先启动了线程,然后再由jvm去调用该线程的run()方法。

7.2.2 实现Runnable接口

    1)定义类实现Runnable接口

    2)覆盖Runnable接口中的run()方法。

    3)通过Thread类建立线程对象。 

    4)将Runnable接口的子类对象作为实际参数传递给Thread类的构造函数。 

    5)调用Thread类的star()t方法开启线程

    示例:

<span style="font-size:18px;">public class MyRunnableDemo {
public static void main(String[] args) {
// 创建MyRunnable类的对象
MyRunnable my = new MyRunnable();

// Thread(Runnable target, String name)
Thread t1 = new Thread(my);
Thread t2 = new Thread(my);

t1.start();
t2.start();
}
}

class MyRunnable implements Runnable {

@Override
public void run() {
for (int x = 0; x < 100; x++) {
System.out.println(x);
}
}

}</span>


7.3 多线程的设置和控制

7.3.1 获取和设置线程对象名

    获取:

    public final String getName():获取线程的名称。

    针对不是Thread类的子类中获取线程对象名的方法

    public static Thread currentThread():返回当前正在执行的线程对象

    Thread.currentThread().getName()

    设置:

    1)public final void setName(String name):设置线程的名称

    2)通过构造方法

public class MyThreadDemo {
public static void main(String[] args) {
// 创建线程对象
// 无参构造+setXxx()
MyThread my1 = new MyThread();
MyThread my2 = new MyThread();

// 调用set方法设置名称
my1.setName("林青霞");
my2.setName("东方不败");

my1.start();
my2.start();

// 带参构造方法给线程起名字
MyThread my3 = new MyThread("李亚鹏");
MyThread my4 = new MyThread("令狐冲");

my3.start();
my4.start();

// 我要获取main方法所在的线程对象的名称,该怎么办呢?
// 遇到这种情况,Thread类提供了一个很好玩的方法:
// public static Thread currentThread():返回当前正在执行的线程对象
System.out.println(Thread.currentThread().getName());
}
}

class MyThread extends Thread {
public MyThread() {
}
// 通过构造方法设置对象名
public MyThread(String name) {
super(name);
}

@Override
public void run() {
for (int x = 0; x < 100; x++) {
// 获取对象名
System.out.println(getName() + ":" + x);
}
}
}

7.3.2 获取和设置线程优先级

    假如计算机只有一个 CPU,那么 CPU 在某一个时刻只能执行一条指令,线程只有得到 CPU时间片,也就是使用权,才可以执行指令。那么Java是如何对线程进行调用的呢?

    线程有两种调度模型:

    分时调度模型    所有线程轮流使用 CPU 的使用权,平均分配每个线程占用 CPU 的时间片

    抢占式调度模型    优先让优先级高的线程使用 CPU,如果线程的优先级相同,那么会随机选择一个,优先级高的线程获取的 CPU 时间片相对多一些。

    Java使用的是抢占式调度模型。

    获取线程对象的优先级:

    public final int getPriority():返回线程对象的优先级

    设置线程对象的优先级

    public final void setPriority(int newPriority):更改线程的优先级。线程默认优先级是5(NORM_PRIORITY),可设置线程优先级的范围是:1(MIN_PRIORITY)-10(MAX_PRIORITY)。 

public class ThreadPriorityDemo {
public static void main(String[] args) {
ThreadPriority tp1 = new ThreadPriority();
ThreadPriority tp2 = new ThreadPriority();
ThreadPriority tp3 = new ThreadPriority();

tp1.setName("线程1");
tp2.setName("线程2");
tp3.setName("线程3");

//设置线程优先级
tp1.setPriority(Thread.MAX_PRIORITY);
tp2.setPriority(Thread.MIN_PRIORITY);
tp3.setPriority(Thread.NORM_PRIORITY);

tp1.start();
tp2.start();
tp3.start();

}
}

class ThreadPriority extends Thread{
@Override
public void run() {
for(int x =0;x<100;x++){
System.out.println(getName()+":"+x);
}
}
}


    运行后发现基本上先是线程1线执行的,最后是线程3执行的。

7.3.3 线程休眠(sleep)

    public static void sleep(long millis)

    示例:

import java.util.Date;

public class ThreadSleepDemo {
public static void main(String[] args) {
ThreadSleep ts1 = new ThreadSleep();
ThreadSleep ts2 = new ThreadSleep();
ThreadSleep ts3 = new ThreadSleep();

ts1.setName("线程1");
ts2.setName("线程2");

ts1.start();
ts2.start();
}
}

class ThreadSleep extends Thread{
@Override
public void run() {
for (int x = 0; x < 2; x++) {
System.out.println(getName() + ":" + x+",日期:"+new Date());
//睡眠
//休眠1秒钟
try {
Thread.sleep(1000);
} catch (InterruptedException e) {
e.printStackTrace();
}
}
}
}


    运行结果:



7.3.4 线程加入(join)

    public final void join():等待该线程终止。必须用在该线程启动后。 

    示例:

public class ThreadJoinDemo {
public static void main(String[] args) {
ThreadJoin tj1 = new ThreadJoin();
ThreadJoin tj2 = new ThreadJoin();
ThreadJoin tj3 = new ThreadJoin();

tj1.setName("李渊");
tj2.setName("李世民");
tj3.setName("李元霸");

tj1.start();
try {
tj1.join();
} catch (InterruptedException e) {
e.printStackTrace();
}

tj2.start();
tj3.start();
}
}

class ThreadJoin extends Thread {
@Override
public void run() {
for (int x = 0; x < 100; x++) {
System.out.println(getName() + ":" + x);
}
}
}


    运行后发现在线程“李渊”运行完成后才是线程“李世民”和“李元霸”在争夺CPU执行权

7.3.5 线程礼让(yield)

    public static void yield():将当前线程变为可执行状态,然后与其他线程再次抢夺CPU的执行权。 可以使让多个线程的执行更和谐,但是不能靠它来保证一人一次。

    示例:

public class ThreadYieldDemo {
public static void main(String[] args) {
ThreadYield ty1 = new ThreadYield();
ThreadYield ty2 = new ThreadYield();

ty1.setName("令狐冲");
ty2.setName("任盈盈");

ty1.start();
ty2.start();
}
}

class ThreadYield extends Thread {
@Override
public void run() {
for (int x = 0; x < 100; x++) {
System.out.println(getName() + ":" + x);
Thread.yield();
}
}
}


    运行结果:



7.3.6 后台线程(setDaemon)

    public final void setDaemon(boolean on):将该线程标记为守护线程或用户线程。当正在运行的线程都是守护线程时,Java 虚拟机退出。该方法必须在启动线程前调用。 

    示例:

public class ThreadDaemonDemo {
public static void main(String[] args) {
ThreadDaemon td1 = new ThreadDaemon();
ThreadDaemon td2 = new ThreadDaemon();

td1.setName("关羽");
td2.setName("张飞");

// 设置收获线程
td1.setDaemon(true);
td2.setDaemon(true);

td1.start();
td2.start();

Thread.currentThread().setName("刘备");
for (int x = 0; x < 5; x++) {
System.out.println(Thread.currentThread().getName() + ":" + x);
}
}
}

class ThreadDaemon extends Thread{
@Override
public void run() {
for (int x = 0; x < 100; x++) {
System.out.println(getName() + ":" + x);
}
}
}


    运行结果:



    由于“关羽”线程和“张飞”线程被设置为后台线程,所以在其他线程运行完成后,这两个后台线程也会停止运行。

7.3.7 中断线程(stop/interrupt)

    public final void stop():让线程停止,过时了,但是现在还可以使用。

     public void interrupt():中断线程。 把线程的状态终止,并抛出一个InterruptedException。

import java.util.Date;

public class ThreadStopDemo {
public static void main(String[] args) {
ThreadStop ts = new ThreadStop();
ts.start();

// 你超过三秒不醒过来,我就干死你
try {
Thread.sleep(3000);
// ts.stop();
ts.interrupt();
} catch (InterruptedException e) {
e.printStackTrace();
}
}
}

class ThreadStop extends Thread {
@Override
public void run() {
System.out.println("开始执行:" + new Date());

// 我要休息10秒钟,亲,不要打扰我哦
try {
Thread.sleep(10000);
} catch (InterruptedException e) {
// e.printStackTrace();
System.out.println("线程被终止了");
}

System.out.println("结束执行:" + new Date());
}
}


    运行结果:



7.3.8 线程组

    线程组: 把多个线程组合到一起。它可以对一批线程进行分类管理,Java允许程序直接对线程组进行控制。

public class ThreadGroupDemo {
public static void main(String[] args) {
// 创建两个线程并查看所属默认线程组
method1();

System.out.println("-----------");
// 修改线程所在组
// 创建一个线程组
// 创建其他线程的时候,把其他线程的组指定为我们自己新建线程组
method2();
}

private static void method2() {
// ThreaadGroup(String name)
ThreadGroup tg = new ThreadGroup("这是一个新的组");

MyRunnable my = new MyRunnable();
// Thread(ThreadGroup group, Runnable target, String name)
Thread t1 = new Thread(tg, my, "林青霞");
Thread t2 = new Thread(tg, my, "东方不败");

System.out.println(t1.getThreadGroup().getName());
System.out.println(t2.getThreadGroup().getName());

// 通过组名称设置后台线程,表示该组的线程都是后台线程
tg.setDaemon(true);
}

private static void method1() {
MyRunnable my = new MyRunnable();
Thread t1 = new Thread(my, "林青霞");
Thread t2 = new Thread(my, "东方不败");
// 我不知道他们属于那个线程组,我想知道,怎么办
// 线程类里面的方法:public final ThreadGroup getThreadGroup()
ThreadGroup tg1 = t1.getThreadGroup();
ThreadGroup tg2 = t2.getThreadGroup();
// 线程组里面的方法:public final String getName()
String name1 = tg1.getName();
String name2 = tg2.getName();
System.out.println(name1);
System.out.println(name2);
// 通过结果我们知道了:线程默认情况下属于main线程组
// 通过下面的测试,你应该能够看到,默任情况下,所有的线程都属于同一个组
System.out.println(Thread.currentThread().getThreadGroup().getName());
}
}
class MyRunnable implements Runnable{
@Override
public void run() {
for(int x = 0;x<100;x++){
System.out.println(Thread.currentThread().getName()+":"+x);
}
}
}


    运行结果:



7.4 多线程安全问题

    例:模拟电影院有3个窗口卖票案例

public class SellTicketDemo {
public static void main(String[] args) {
// 创建3个线程对象
SellTicket st1 = new SellTicket();
SellTicket st2 = new SellTicket();
SellTicket st3 = new SellTicket();

// 设置线程名称
st1.setName("窗口1");
st2.setName("窗口2");
st3.setName("窗口3");

// 启动线程
st1.start();
st2.start();
st3.start();
}
}
class SellTicket extends Thread {

// 定义100张票
// private int tickets = 100;
// 为了让多个线程对象共享这100张票,我们其实应该用静态修饰
private static int tickets = 100;

@Override
public void run() {
// 定义100张票
// 每个线程进来都会走这里,这样的话,每个线程对象相当于买的是自己的那100张票,这不合理,所以应该定义到外面
// int tickets = 100;

// 是为了模拟一直有票
while (tickets > 0) {
System.out.println(getName() + "正在出售第" + (tickets--) + "张票");
}
}
}


    由于100张票是共享资源,上面用SellTicket类继承Thread类来实现3个窗口卖票要建立3个SellTicket类对象,不太符合现实。通过继承Runnable接口来做这种共享资源的示例会方便很多。代码如下:

public class SellTicketDemo {
public static void main(String[] args) {
//创建资源对象
SellTicket st = new SellTicket();

//创建3个线程对象
Thread t1 = new Thread(st,"窗口1");
Thread t2 = new Thread(st,"窗口2");
Thread t3 = new Thread(st,"窗口3");

//启动线程
t1.start();
t2.start();
t3.start();
}
}
class SellTicket implements Runnable {
//定义100张票
private int tickets = 100;
@Override
public void run() {
while(tickets>0){
System.out.println(Thread.currentThread().getName()+"正在出第"+(tickets--)+"票");
}
}

}


    在现实生活中,售票时网络不能实时传输,总会存在一点点的延迟,所以在出售一张票以后,需要一点时间的延续,假定每次卖一张票后延时100ms。

public class SellTicketDemo {
public static void main(String[] args) {
// 创建资源对象
SellTicket st = new SellTicket();

// 创建三个线程对象
Thread t1 = new Thread(st, "窗口1");
Thread t2 = new Thread(st, "窗口2");
Thread t3 = new Thread(st, "窗口3");

// 启动线程
t1.start();
t2.start();
t3.start();
}
}
class SellTicket implements Runnable {
// 定义100张票
private int tickets = 100;

@Override
public void run() {
while (true) {
// t1,t2,t3三个线程
// 这一次的tickets = 1;
if (tickets > 0) {
// 为了模拟更真实的场景,我们稍作休息
try {
Thread.sleep(100); //t1进来了并休息,t2进来了并休息,t3进来了并休息,
} catch (InterruptedException e) {
e.printStackTrace();
}

System.out.println(Thread.currentThread().getName() + "正在出售第"
+ (tickets--) + "张票");
//窗口1正在出售第1张票,tickets=0
//窗口2正在出售第0张票,tickets=-1
//窗口3正在出售第-1张票,tickets=-2
}
}
}
}


    运行结果:



    发现打印出0、-1等错票,说明多线程存在安全问题。

7.4.1 出错原因和出错位置

    1. 出错原因:

    当多条语句在操作多个线程共享数据时,如果一个线程对多条语句只执行了一部分,还没有执行完,另一个线程参与进来执行,就会导致共享数据出错。

    2. 出错位置:

    1)明确哪些代码是多线程运行代码。 

    2)明确共享数据。 

    3)明确多线程运行代码中哪些语句是操作共享数据的。

7.2.2 解决办法

    把多条线程操作共享数据的代码给包成一个整体,让某个线程在执行的时候,别人不能来执行。Java提供了对象锁机制来解决

    1. synchronized关键字

    1)synchronized代码块

    格式:

    synchronized(对象){

        需要同步的代码

    }

    使用synchronized的前提:必须是两个或两个以上的线程使用同一个锁

    如果加了synchronized后仍然出现安全问题,就应该考虑上面的前提是否满足。

public class SellTicketDemo {
public static void main(String[] args) {
// 创建资源对象
SellTicket st = new SellTicket();

// 创建三个线程对象
Thread t1 = new Thread(st, "窗口1");
Thread t2 = new Thread(st, "窗口2");
Thread t3 = new Thread(st, "窗口3");

// 启动线程
t1.start();
t2.start();
t3.start();
}
}
class SellTicket implements Runnable {
// 定义100张票
private int tickets = 100;
//创建锁对象
private Object obj = new Object();

@Override
public void run() {
while (true) {
synchronized (obj) {
if (tickets > 0) {
try {
Thread.sleep(100);
} catch (InterruptedException e) {
e.printStackTrace();
}
System.out.println(Thread.currentThread().getName()
+ "正在出售第" + (tickets--) + "张票");
}
}
}
}
}


    运行结果:



    2)synchronized方法

    格式:synchronized void method(){}

public class SellTicketDemo {
public static void main(String[] args) {
// 创建资源对象
SellTicket st = new SellTicket();

// 创建三个线程对象
Thread t1 = new Thread(st, "窗口1");
Thread t2 = new Thread(st, "窗口2");
Thread t3 = new Thread(st, "窗口3");

// 启动线程
t1.start();
t2.start();
t3.start();
}
}
class SellTicket implements Runnable {

// 定义100张票
private static int tickets = 100;

private int x = 0;

@Override
public void run() {
while (true) {
if(x%2==0){
synchronized (this) {
if (tickets > 0) {
try {
Thread.sleep(100);
} catch (InterruptedException e) {
e.printStackTrace();
}
System.out.println(Thread.currentThread().getName()
+ "正在出售第" + (tickets--) + "张票 ");
}
}
}else {
sellTicket();

}
x++;
}
}
private synchronized void sellTicket() {
if (tickets > 0) {
try {
Thread.sleep(100);
} catch (InterruptedException e) {
e.printStackTrace();
}
System.out.println(Thread.currentThread().getName()
+ "正在出售第" + (tickets--) + "张票 ");
}
}
}


    运行结果:



    由此可以看出synchronized方法的所对象是this。如果synchronized方法再加上static修饰后,锁对象是什么呢?由于静态进内存时,内存中没有本类对象,但是一定有该类对应的字节码文件对象,所以该对象就成了静态同步方法的锁对象。

    2. Lock锁

    为了更清晰的表达如何加锁和释放锁,JDK5以后提供了一个新的接口Lock。Lock中的lock()方法用来加锁,unlock(()方法用来解锁。

import java.util.concurrent.locks.Lock;
import java.util.concurrent.locks.ReentrantLock;

/*
* 虽然我们可以理解同步代码块和同步方法的锁对象问题,但是我们并没有直接看到在哪里加上了锁,在哪里释放了锁,
* 为了更清晰的表达如何加锁和释放锁,JDK5以后提供了一个新的锁对象Lock。
*
* Lock:
* 		void lock(): 获取锁。
* 		void unlock():释放锁。
* ReentrantLock是Lock的实现类.
*/
public class SellTicketDemo {
public static void main(String[] args) {
// 创建资源对象
SellTicket st = new SellTicket();

// 创建3个线程
Thread t1 = new Thread(st, "窗口1");
Thread t2 = new Thread(st, "窗口2");
Thread t3 = new Thread(st, "窗口3");

// 启动线程
t1.start();
t2.start();
t3.start();
}
}
class SellTicket implements Runnable {
// 定义票
private int tickets = 100;
// 定义锁对象 ReentrantLock类是Lock接口的实现类
private Lock lock = new ReentrantLock();

@Override
public void run() {
while (true) {
try {
// 加锁
lock.lock();
if (tickets > 0) {
try {
Thread.sleep(100);
} catch (InterruptedException e) {
e.printStackTrace();
}
System.out.println(Thread.currentThread().getName()
+ "正在出售第" + (tickets--) + "张票");
}
} finally {
// 释放锁
lock.unlock();
}
}
}
}


    运行结果:



7.4.3 死锁

    两个或两个以上的线程在争夺资源的过程中,发生的一种相互等待的现象。

    例如:

public class DieLock extends Thread {
private boolean flag;

public DieLock(boolean flag){
this.flag = flag;
}
@Override
public void run() {
if(flag){
synchronized (MyLock.objA){
System.out.println("if objA");
synchronized(MyLock.objB){
System.out.println("if objB");
}
}
}
else{
synchronized (MyLock.objB){
System.out.println("else objB");
synchronized(MyLock.objA){
System.out.println("else objA");
}
}
}
}

}

class MyLock {
// 创建两把锁对象
public static final Object objA = new Object();
public static final Object objB = new Object();
}

class DieLockDemo {
public static void main(String[] args) {
DieLock dl1 = new DieLock(true);
DieLock dl2 = new DieLock(false);

dl1.start();
dl2.start();
}
}


    运行结果:



7.5 多线程通信问题

    多线程的通信问题:不同种类的线程对同一资源的操作。

    示例:

public class StudentDemo {
public static void main(String[] args) {
//创建资源
Student s = new Student();

//设置和获取的类
SetThread st = new SetThread(s);
GetThread gt = new GetThread(s);

//线程类
Thread t1 = new Thread(st);
Thread t2 = new Thread(gt);

//启动线程
t1.start();
t2.start();
}
}
class Student {
String name;
int age;
}
class SetThread implements Runnable {

private Student s;
private int x = 0;

public SetThread(Student s) {
this.s = s;
}

@Override
public void run() {
while (true) {
synchronized (s) {
if (x % 2 == 0) {
s.name = "林青霞";//刚走到这里,就被别人抢到了执行权
s.age = 27;
} else {
s.name = "东方不败"; //刚走到这里,就被别人抢到了执行权
s.age = 30;
}
x++;
}
}
}
}
class GetThread implements Runnable {
private Student s;

public GetThread(Student s) {
this.s = s;
}

@Override
public void run() {
while (true) {
synchronized (s) {
System.out.println(s.name + "---" + s.age);
}
}
}
}


    运行结果:



    上面的示例代码中有两处不足:一是设置和获取的操作分别放在了SetThread和GetThread类中,如果放在Student类中会更好些;二是“东方不败----30”和 “林青霞----27”没有依次轮流出现。解决第一个问题可以将设置和获取操作封装成功能放到Student类中。为了解决第二个问题,Java提供了等待唤醒机制。

    Object类中提供了3个方法:

    wait():等待

    notify():唤醒单个线程

    notifyAll():唤醒所有线程

    这些方法的调用者必须是锁对象,所以将这三个方法放在了Object类中。

public class StudentDemo {
public static void main(String[] args) {
// 创建资源
Student s = new Student();

// 设置和获取的类
SetThread st = new SetThread(s);
GetThread gt = new GetThread(s);

// 线程类
Thread t1 = new Thread(st);
Thread t2 = new Thread(gt);

// 启动线程
t1.start();
t2.start();
}
}
class Student {
private String name;
private int age;
private boolean flag; // 默认情况是没有数据,如果是true,说明有数据

public synchronized void set(String name, int age) {
// 如果有数据就等待
if (this.flag) {
try {
this.wait();
} catch (InterruptedException e) {
e.printStackTrace();
}
}
// 设置数据
this.name = name;
this.age = age;

// 修改标记
this.flag = true;
this.notify();

}

public synchronized void get() {
// 如果没有数据就等待
if (!this.flag) {
try {
this.wait();
} catch (InterruptedException e) {
e.printStackTrace();
}
}
// 获得数据
System.out.println(this.name + "--" + this.age);

// 修改标记
this.flag = false;
this.notify();
}
}
class SetThread implements Runnable {

private Student s;
private int x = 0;

public SetThread(Student s) {
this.s = s;
}

@Override
public void run() {
while (true) {
if(x%2==0){
s.set("林青霞", 27);
}
else{
s.set("东方不败",30);
}
x++;
}
}
}
class GetThread implements Runnable {
private Student s;

public GetThread(Student s) {
this.s = s;
}

@Override
public void run() {
while (true) {
s.get();
}
}
}


    运行结果:



7.6 线程间的状态转换图



    面试题1:sleep和wait的区别

    1)sleep是Thread类里的方法;wait是Object类里的方法

    2)sleep比wait的使用范围广,因为wait只能在synchronized方法或synchronized代码块中使用

    3)sleep后面必须跟一个时间,且调用sleep方法后线程不会释放锁;wait后面可以不跟时间,调用wait方法后线程会释放锁

   面试题2:join和yield的区别

    1)join是等待该线程运行结束后再继续往下走。例如t.join()就是说t线程运行完后,JVM才会继续往下走。

    2)yield是让当前线程重新回到就绪状态。执行完后该线程会与其他线程继续抢夺CPU的执行权。

7.7 线程池

7.7.1 线程池出现的原因

    程序启动一个新线程成本是比较高的,因为它涉及到要与操作系统进行交互。而使用线程池可以很好的提高性能,尤其是当程序中要创建大量生存期很短的线程时,更应该考虑使用线程池。

7.7.2 线程池的好处

    线程池里的每一个线程代码结束后,并不会死亡,而是再次回到线程池中成为空闲状态,等待下一个对象来使用。

7.7.3 实现线程池的步骤

    步骤:

    1)创建一个线程池对象,控制要创建几个线程对象。

         public static ExecutorService newFixedThreadPool(int nThreads)

    2)线程池可以执行Runnable对象或者Callable对象代表的线程

    3)结束线程池

        void shutdown()

    示例1:

public class ExecutorsDemo {
public static void main(String[] args) {
// 创建一个线程池对象,控制要创建几个线程对象。
// public static ExecutorService newFixedThreadPool(int nThreads)
ExecutorService pool = Executors.newFixedThreadPool(2);

// 可以执行Runnable对象或者Callable对象代表的线程
pool.submit(new MyRunnable());
pool.submit(new MyRunnable());

// 结束线程池
pool.shutdown();
}
}
class MyRunnable implements Runnable{
@Override
public void run() {
for(int x = 0;x<100;x++){
System.out.println(Thread.currentThread().getName()+":"+x);
}
}
}


    运行结果:



    示例2:

public class CallableDemo {
public static void main(String[] args) {
//创建线程池对象
ExecutorService pool = Executors.newFixedThreadPool(2);

//可以执行Runnable对象或者Callable对象代表的线程
pool.submit(new MyCallable());
pool.submit(new MyCallable());

//结束
pool.shutdown();
}
}
class MyCallable implements Callable<Object> {

@Override
public Object call() throws Exception {
for (int x = 0; x < 100; x++) {
System.out.println(Thread.currentThread().getName() + ":" + x);
}
return null;
}

}


    运行结果:



    练习:同时求出1-100和1-200的和,并输出到控制台。

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 MyCallableDemo {
public static void main(String[] args) throws InterruptedException, ExecutionException {
// 创建线程吃对象
ExecutorService pool = Executors.newFixedThreadPool(2);

// 执行Runnable对象或者Callable对象代表的线程
Future<Integer> f1 = pool.submit(new MyCallable(100));
Future<Integer> f2 = pool.submit(new MyCallable(200));

// V get()
Integer i1 = f1.get();
Integer i2 = f2.get();

System.out.println(i1);
System.out.println(i2);
// 结束
pool.shutdown();
}
}
class MyCallable implements Callable<Integer> {

private int number;

public MyCallable() {
super();
}

public MyCallable(int number) {
super();
this.number = number;
}

@Override
public Integer call() throws Exception {
int sum = 0;
for(int x = 0;x<=number;x++){
sum+=x;
}
return sum;
}

}


    运行结果:



7.8 匿名内部类实现多线程

    示例:

public class ThreadDemo {
public static void main(String[] args) {
// 继承Thread类来实现多线程
new Thread() {
public void run() {
for (int x = 0; x < 100; x++) {
System.out.println(Thread.currentThread().getName() + ":"
+ x);
}
}
}.start();

// 实现Runnable接口来实现多线程
new Thread(new Runnable() {

@Override
public void run() {
for (int x = 0; x < 100; x++) {
System.out.println(Thread.currentThread().getName() + ":"
+ x);
}
}

}) {
}.start();
}
}
    运行结果:



    线程0和线程1在抢夺CPU资源。

    面试题:下面的代码会报错么?如果不会,运行结果是什么?

public class ThreadDemo {
public static void main(String[] args) {
// 更有难度的一个。下面的程序会执行
new Thread(new Runnable() {

@Override
public void run() {
for (int x = 0; x < 100; x++) {
System.out.println("hello" + ":" + x);
}
}

}) {
public void run() {
for (int x = 0; x < 100; x++) {
System.out.println("world" + ":" + x);
}
}
}.start();
}
}


    上面的代码其实是两个匿名内部类实现了多线程,其中一个是继承Thread类来实现的,另一个是实现Runnable接口来实现的。

    运行结果:



7.9 定时器

    定时器可以让我们在指定的时间做某件事情,还可以重复的做某件事情。定时器依赖Timer类和TimerTask接口。

    Timer:

    public void schedule(TimerTask task,long delay)

    public void schedule(TimerTask task,long delay,long period)

    public void cancel()

    TimerTask:

    public abstract void run():此计时器任务要执行的操作。

    示例:

public class TimerDemo {
public static void main(String[] args) {
// 创建定时器对象
Timer t = new Timer();
// 3秒后执行爆炸
// t.schedule(new MyTask(), 3000);
// 3秒后执行爆炸并结束任务
t.schedule(new MyTask(t), 3000);
}
}

// 做一个任务
class MyTask extends TimerTask {

private Timer t;

public MyTask() {
}

public MyTask(Timer t) {
this.t = t;
}

@Override
public void run() {
System.out.println("beng,爆炸了");
t.cancel();
}
}


    运行结果:



    示例2:

import java.util.Timer;
import java.util.TimerTask;

public class TimerDemo2 {
public static void main(String[] args) {
// 创建定时器对象
Timer t = new Timer();
// 3秒后执行爆炸任务第一次,每隔2秒再继续炸
t.schedule(new MyTask2(), 3000, 2000);
}
}

// 做一个任务
class MyTask2 extends TimerTask {
@Override
public void run() {
System.out.println("beng,爆炸了");
}
}


    运行结果:



    练习:在指定的时间删除我们的指定目录

import java.io.File;
import java.text.ParseException;
import java.text.SimpleDateFormat;
import java.util.Date;
import java.util.Timer;
import java.util.TimerTask;

class DeleteFolder extends TimerTask {

private Timer t;

public DeleteFolder() {}

public DeleteFolder(Timer t) {
this.t = t;
}

@Override
public void run() {
File srcFolder = new File("demo");
deleteFolder(srcFolder);
t.cancel();
}

// 递归删除目录
private void deleteFolder(File srcFolder) {
File[] fileArray = srcFolder.listFiles();
if (fileArray != null) {
for (File file : fileArray) {
if (file.isDirectory()) {
deleteFolder(file);
} else {
System.out.println(file.getName() + ":" + file.delete());
}
}
System.out.println(srcFolder.getName() + ":" + srcFolder.delete());
}
}
}

public class TimerTest {
public static void main(String[] args) throws ParseException {
Timer t = new Timer();

String s = "2015-9-18 9:15:50";
SimpleDateFormat sdf = new SimpleDateFormat("yy-MM-dd HH:mm:ss");
Date d = sdf.parse(s);

t.schedule(new DeleteFolder(t), d);
}
}


7.10 线程不安全类转为线程安全类

    常见的线程安全类有:StringBuffer、Vector、Hashtable。虽然使用线程安全类不会有多线程的安全问题,但是效率有点低。Collections类提供了一个synchronizedXxx的方法,可以通过该方法建立一个安全的线程类。

    示例:建立一个线程安全的ArrayList类

import java.util.ArrayList;
import java.util.Collections;
import java.util.Hashtable;
import java.util.List;
import java.util.Vector;

public class ThreadDemo {
public static void main(String[] args) {
// 线程安全的类
StringBuffer sb = new StringBuffer();
Vector<String> v = new Vector<String>();
Hashtable<String, String> h = new Hashtable<String, String>();

// Vector是线程安全的时候才去考虑使用的,但是我还说过即使要安全,我也不用你
// 那么到底用谁呢?
// public static <T> List<T> synchronizedList(List<T> list)
List<String> list1 = new ArrayList<String>();// 线程不安全
List<String> list2 = Collections
.synchronizedList(new ArrayList<String>());// 线程安全
}
}


8. 面向对象增强

8.1 面向对象设计思想

    在实际的开发中,我们要想更深入的了解面向对象思想,就必须熟悉前人总结过的面向对象的思想的设计原则,包括:单一职责原则、开闭原则、里氏替换原则、依赖注入原则、接口分离原则、迪米特原则。

    单一职责原则

    其实就是开发人员经常说的”高内聚,低耦合”。也就是说,每个类应该只有一个职责,对外只能提供一种功能,而引起类变化的原因应该只有一个。在设计模式中,所有的设计模式都遵循这一原则。

    开闭原则

    核心思想是:一个对象对扩展开放,对修改关闭。其实开闭原则的意思就是:对类的改动是通过增加代码进行的,而不是修改现有代码。也就是说软件开发人员一旦写出了可以运行的代码,就不应该去改动它,而是要保证它能一直运行下去,如何能够做到这一点呢?这就需要借助于抽象和多态,即把可能变化的内容抽象出来,从而使抽象的部分是相对稳定的,而具体的实现则是可以改变和扩展的。

    里氏替换原则

    核心思想:在任何父类出现的地方都可以用它的子类来替代。其实就是说:同一个继承体系中的对象应该有共同的行为特征。

    依赖注入原则

    核心思想:要依赖于抽象,不要依赖于具体实现。其实就是说:在应用程序中,所有的类如果使用或依赖于其他的类,则应该依赖这些其他类的抽象类,而不是这些其他类的具体类。为了实现这一原则,就要求我们在编程的时候针对抽象类或者接口编程,而不是针对具体实现编程。

    接口分离原则

    核心思想:不应该强迫程序依赖它们不需要使用的方法。其实就是说:一个接口不需要提供太多的行为,一个接口应该只提供一种对外的功能,不应该把所有的操作都封装到一个接口中。

    迪米特原则

    核心思想:一个对象应当对其他对象尽可能少的了解。其实就是说:降低各个对象之间的耦合,提高系统的可维护性。在模块之间应该只通过接口编程,而不理会模块的内部工作原理,它可以使各个模块耦合度降到最低,促进软件的复用。

8.2 设计模式

8.2.1 设计模式概述

    设计模式(Design pattern)是一套被反复使用、多数人知晓的、经过分类编目的、代码设计经验的总结。使用设计模式是为了可重用代码、让代码更容易被他人理解、保证代码可靠性。

    设计模式不是一种方法和技术,而是一种思想。

    设计模式和具体的语言无关,学习设计模式就是要建立面向对象的思想,尽可能的面向接口编程,低耦合,高内聚,使设计的程序可复用。

    学习设计模式能够促进对面向对象思想的理解,反之亦然。它们相辅相成。

8.2.2 设计模式的几个要素

    名字:必须有一个简单,有意义的名字

    问题:描述在何时使用模式

    解决方案:描述设计的组成部分以及如何解决问题

    效果:描述模式的效果以及优缺点

8.2.3 设计模式的分类

    创建型模式:对象的创建

    结构型模式:对象的组成(结构)

    行为型模式:对象的行为

8.2.4 几个常见的设计模式

1. 简单工厂模式

    1)简单工厂模式概述

    简单工厂模式又叫静态工厂方法模式,它定义一个具体的工厂类负责创建一些类的实例。

    2)简单工厂模式代码实现

public class AnimalDemo {
public static void main(String[] args) {
// 使用工厂
Animal a = AnimalFactory.creatAnimal("dog");
a.eat();
a = AnimalFactory.creatAnimal("cat");
a.eat();

// NullPointerException
a = AnimalFactory.creatAnimal("pig");
if (a != null) {
a.eat();
} else {
System.out.println("对不起,暂时不提供这种动物");
}

}
}
class AnimalFactory {
private AnimalFactory(){};

public static Animal creatAnimal(String type){
if("dog".equals(type)){
return new Dog();
}else if("cat".equals(type)){
return new Cat();
}else{
return null;
}
}
}
abstract class Animal {
public abstract void eat();
}
class Dog extends Animal {

@Override
public void eat() {
System.out.println("狗吃肉");
}

}
class Cat extends Animal {

@Override
public void eat() {
System.out.println("猫吃鱼");
}
}


    运行结果:



    3)简单工厂模式优缺点

    优点:客户端不需要在负责对象的创建,从而明确了各个类的职责。

    缺点:这个静态工厂类负责所有对象的创建,如果有新的对象增加,或者某些对象的创建方式不同,就需要不断的修改工厂类,不利于后期的维护。

2. 工厂方法模式

    1)工厂方法模式概述

    工厂方法模式中抽象工厂类负责定义创建对象的接口,具体对象的创建工作由继承抽象工厂的具体类实现。

    2)工厂方法模式代码实现

public class AnimalDemo {
public static void main(String[] args) {
// 需求:我要买只狗
Factory f= new DogFactory();
Animal a = f.creatAnimal();
a.eat();
System.out.println("-------");
//需求:我要买只猫
f = new CatFactory();
a = f.creatAnimal();
a.eat();
}
}
interface Factory {
public abstract Animal creatAnimal();
}
class DogFactory implements Factory {

@Override
public Animal creatAnimal() {
return new Dog();
}

}
class CatFactory implements Factory {

@Override
public Animal creatAnimal() {
return new Cat();
}

}
public abstract class Animal {
public abstract void eat();
}
class Dog extends Animal {

@Override
public void eat() {
System.out.println("狗吃肉");
}

}
class Cat extends Animal {

@Override
public void eat() {
System.out.println("猫吃鱼");
}

}


    运行结果:



    3)工厂方法模式优缺点

    优点:客户端不需要在负责对象的创建,从而明确了各个类的职责,如果有新的对象增加,只需要增加一个具体的类和具体的工厂类即可,不影响已有的代码,后期维护容易,增强了系统的扩展性 

    缺点:需要额外的编写代码,增加了工作量

3. 单例设计模式

    1)单例设计模式概述

    单例模式就是要确保类在内存中只有一个对象,该实例必须自动创建,并且对外提供。

    2)单例设计模式代码实现

    步骤:a、把构造方法私有b、在成员位置自己创建一个对象c、通过一个公共的方法提供访问

   
饿汉式:

public class StudentDemo {
public static void main(String[] args) {

// 通过单例设计模式得到对象
Student s1 = Student.getStudent();
Student s2 = Student.getStudent();
System.out.println(s1 == s2);
}
}
class Student {
// 构造方法私有
private Student() {
};

// 在成员位置自己创建一个对象
// 静态方法只能访问静态成员变量,加静态
// 为了不让外界直接访问修改这个值,加private
private static Student s = new Student();

// 通过一个公共的方法提供访问
public static Student getStudent() {
return s;
}
}


    运行结果:



    懒汉式:

public class Teacher {
private Teacher() {
}

private static Teacher t = null;

public synchronized static Teacher getTeacher() {

if (t == null) {
t = new Teacher();
}
return t;
}
}
class TeacherDemo {
public static void main(String[] args) {
Teacher t1 = Teacher.getTeacher();
Teacher t2 = Teacher.getTeacher();
System.out.println(t1 == t2);
}
}


    运行结果:


    

    注意:开发的时候用饿汉式,面试的时候更多会问懒汉式相关的问题。

    3)单例设计模式优缺点

    优点:在系统内存中只存在一个对象,因此可以节约系统资源,对于一些需要频繁创建和销毁的对象单例模式无疑可以提高系统的性能。

    缺点:没有抽象层,因此扩展很难。职责过重,在一定程序上违背了单一职责。

    4)单例设计模式在Java源码中的应用

    Java源码中的Runtime类中创建新对象就用到了饿汉式。

    Runtime:每个 Java 应用程序都有一个 Runtime 类实例,使应用程序能够与其运行的环境相连接。其中有一个方法是exec(String command):在单独的进程中执行指定的字符串命令

    示例:

import java.io.IOException;

public class RuntimeDemo {
public static void main(String[] args) throws IOException {
Runtime r = Runtime.getRuntime();
// r.exec("winmine");
// r.exec("notepad");//打开记事本
// r.exec("calc");//打开计算器
r.exec("shutdown -s -t 1000");// 定时关机
r.exec("shutdown a");// 取消关机命令
}
}


    
内容来自用户分享和网络整理,不保证内容的准确性,如有侵权内容,可联系管理员处理 点击这里给我发消息