高内聚,低耦合
所谓高内聚,低耦合就是每个程序要实现的特定功能要尽量多的在自己的内部完成,对外部提供很少量的接口以便调用。
并行和并发
并行即多个任务在自己的分配的各自的资源上执行,即多个任务同时跑,互不冲突。
并发即多个任务在统一的一个资源内执行,多个任务会抢占同一资源,在cpu中体现为交替执行,并不能同时执行,但是cpu在多个线程中切换的速度会非常快,所以宏观上看似是同时的。
最大的区别体现在【同时】上。
并行:
并发:
复习多线程创建的四种方式
1.创建线程的方式一:继承Thread类的方式
使用方法:
- 创建一个继承于Thread类的子类
- 重写Thread类的run() --> 将此线程执行的操作声明在run()中
- 创建Thread类的子类的对象
- 通过此对象调用start():①启动当前线程 ② 调用当前线程的run()
- 例子:利用包含main线程在内的三个线程分别遍历10以内所有偶数
//1.创建一个继承于thread类的子类
class MyThread extends Thread{
//2. 重写Thread类的run()方法
@Override
public void run() {
//遍历10以内所有偶数
for (int i = 0; i < 10; i++) {
if(i % 2 == 0){
System.out.println(Thread.currentThread().getName() +":"+ i);
}
}
}
}
public class ThreadTest {
public static void main(String[] args) {
//3. 创建Thread类的子类的对象
MyThread t1 = new MyThread();
//4. 通过此对象调用start() 启动当前线程,调用当前线程的run()
//注意的问题一:我们不能通过直接调用对象.run()启动线程
// t1.run();
t1.start();
//需要重新创建一个对象再start
MyThread t2 = new MyThread();
t2.start();
//如下的操作仍然是在main线程中执行的
System.out.println("hello");
for (int i = 0; i < 10; i++) {
if(i % 2 == 0){
System.out.println(Thread.currentThread().getName() +":"+ i + "********main()*********");
}
}
}
}
运行结果(因为线程由操作系统和cpu调度,多次运行结果不同):
hello
Thread-1:0
Thread-0:0
Thread-1:2
Thread-1:4
Thread-1:6
Thread-1:8
main:0********main()*********
Thread-0:2
main:2********main()*********
Thread-0:4
Thread-0:6
Thread-0:8
main:4********main()*********
main:6********main()*********
main:8********main()*********
2.创建线程的方式二:实现Runnable接口的方式
使用方法:
- 创建一个实现了Runnable接口的类
- 实现类去实现Runnable中的抽象方法:run()
- 创建实现类的对象
- 将此对象作为参数传递到Thread类的构造器中,创建Thread类的对象
- 通过Thread类的对象调用start(),启动线程
实现Runnable和继承Thread方式的对比:
开发中:优先选择:实现Runnable接口的方式
原因:
- 实现接口的方式没有类的单继承性的局限性
- 实现的方式更适合来处理多个线程共享数据的情况。
- 例子
public class ThreadTest1 {
public static void main(String[] args) {
//3. 创建实现类的对象
MThread m = new MThread();
//4. 将此对象作为参数传递到Thread类的构造器中,创建Thread类的对象
Thread thread1 = new Thread(m,"线程一");
//5. 通过Thread类的对象调用start()
thread1.start();
//再启动一个线程,遍历10以内偶数
Thread thread2 = new Thread(m,"线程二");
thread2.start();
}
}
//1. 创建一个实现了Runnable接口的类
class MThread implements Runnable{
//2. 实现类去实现Runnable中的抽象方法:run()
@Override
public void run() {
for (int i = 0; i < 10; i++) {
if(i % 2 == 0){
System.out.println(Thread.currentThread().getName()+":"+i);
}
}
}
}
运行结果:
线程二:0
线程二:2
线程二:4
线程二:6
线程一:0
线程二:8
线程一:2
线程一:4
线程一:6
线程一:8
3.创建线程的方式三:实现Callable接口的方式 ---- jdk5.0新增
使用方法
1.创建一个实现Callable接口的实现类
2.重写call方法,将此线程需要执行的操作声明在call()中
3.创建Callable接口实现类的对象
4.将此Callable接口实现类的对象作为参数传递到FutureTask构造器中,创建FutureTask的对象
5.将FutureTask的对象作为参数传入Thread类的构造器中,创建Thread对象,并调用start()方法。
6.还可以使用FutureTask对象的get()方法获取Callable中的call方法的返回值,不需要返回值的话,在重写的call()方法里返回null即可
如何理解实现Callable接口的方式创建多线程比实现Runnable接口创建多线程方式强大?
- call()方法是可以有返回值的。
- call()可以抛出异常,被外面的操作捕获,获取异常的信息
- Callable是支持泛型的
例子:创建一个以实现Callable方式的线程,内部计算1到100的所有偶数的和
//1.创建一个实现Callable接口的实现类
class NumThread implements Callable<Integer>{
//2.重写call方法,将此线程需要执行的操作声明在call()中
@Override
public Integer call() throws Exception {
int sum = 0;
for (int i = 0; i <= 100; i++) {
if(i % 2 == 0){
sum += i;
}
}
return sum;
}
}
public class ThreadNew {
public static void main(String[] args) {
//3.创建Callable接口实现类的对象
NumThread n = new NumThread();
//4.将此Callable接口实现类的对象作为参数传递到FutureTask构造器中,创建FutureTask的对象
FutureTask<Integer> futureTask = new FutureTask(n);
//5.将FutureTask的对象作为参数传入Thread类的构造器中,创建Thread对象,并调用start()方法。
Thread t = new Thread(futureTask);
t.start();
try {
//6.获取Callable中的call方法的返回值
//get方法返回值即为futureTask构造器参数Callable实现类重写的call()方法的返回值
Integer res = futureTask.get();
System.out.println("总和为"+res);
} catch (InterruptedException e) {
e.printStackTrace();
} catch (ExecutionException e) {
e.printStackTrace();
}
}
}
//运行结果为:总和为2550
创建线程池的方式之四:创建线程池Executor
好处:
1.提高响应速度(减少了创建新线程的时间)
2.降低资源消耗(重复利用线程池中线程,不需要每次都创建)
3.便于线程管理
corePoolSize:核心池的大小
maximumPoolSize:最大线程数
keepAliveTime:线程没任务时最多保持多长时间后会终止
主要有三种 Executor:
CachedThreadPool:一个任务创建一个线程;
FixedThreadPool:所有任务只能使用固定大小的线程;
SingleThreadExecutor:相当于大小为 1 的 FixedThreadPool。
例子:
//建立MyRunnable类实现Runnable并在run()内添加需要的业务逻辑即可,这里省略
public static void main(String[] args) {
ExecutorService executorService = Executors.newCachedThreadPool();
for (int i = 0; i < 5; i++) {
executorService.execute(new MyRunnable());
}
executorService.shutdown();
}
线程的生命周期和优先级
生命周期:
状态
- new(新建)
- runnnable(就绪)
- blocked(阻塞)
- waiting(等待)
- time waiting (定时等待)
- terminated(终止)
线程状态流程如下:
- 线程创建后,进入 new 状态
- 调用 start 方法,进入 runnable 状态
- JVM 按照线程优先级及时间分片等执行 runnable 状态的线程。开始执行时,执行run()方法并进入 running 状态
- 如果线程执行 sleep、wait、join,或者进入 IO 阻塞等。进入 wait 或者 blocked 状态
- 线程执行完毕后,线程被线程队列移除。最后为 terminated 状态
线程的同步方法
方式一:同步代码块
优点:线程安全但是效率较低
注意:同步监视器必须是独一无二的唯一对象。
synchronized(同步监视器){
//需要被同步的代码...
}
非常经典的例子:卖票。
创建三个窗口卖票,总票数为10张,使用实现Runnable接口的方式。
若不加同步:卖票过程中,可能出现重票、错票的线程安全问题。
问题出现的原因:当某个线程操作车票的过程中,尚未操作完成时,其他线程参与进来。
这里就用同步代码块来实现。
public class WindowTest1{
public static void main(String[] args) {
Window1 w = new Window1();
Thread t1 = new Thread(w);
Thread t2 = new Thread(w);
Thread t3 = new Thread(w);
t1.setName("窗口一");
t2.setName("窗口二");
t3.setName("窗口三");
t1.start();
t2.start();
t3.start();
}
}
class Window1 implements Runnable{
private int tickets = 10;
@Override
public void run() {
while(true) {
synchronized(this){
if(tickets > 0){
System.out.println(Thread.currentThread().getName()+"卖票,票号为"+tickets);
tickets--;
}
else{
break;
}
}
}
}
}
//执行结果:没有重票,错票的情况。
//窗口一卖票,票号为10
//窗口一卖票,票号为9
//窗口一卖票,票号为8
//窗口一卖票,票号为7
//窗口一卖票,票号为6
//窗口一卖票,票号为5
//窗口一卖票,票号为4
//窗口一卖票,票号为3
//窗口一卖票,票号为2
//窗口三卖票,票号为1
方式二:声明同步方法
在需要同步的操作共享资源的方法上添加synchronized关键字
同样是卖票的例子:
public class WindowTest3 {
public static void main(String[] args) {
Window3 w = new Window3();
Thread t1 = new Thread(w);
Thread t2 = new Thread(w);
Thread t3 = new Thread(w);
t1.setName("窗口一");
t2.setName("窗口二");
t3.setName("窗口三");
t1.start();
t2.start();
t3.start();
}
}
class Window3 implements Runnable{
private int tickets = 10;
@Override
public void run() {
while(true) {
show();
}
}
private synchronized void show(){ //同步监视器:this
if(tickets > 0){
System.out.println(Thread.currentThread().getName()+"卖票,票号为"+tickets);
tickets--;
}
}
}
//窗口一卖票,票号为10
//窗口一卖票,票号为9
//窗口一卖票,票号为8
//窗口一卖票,票号为7
//窗口一卖票,票号为6
//窗口一卖票,票号为5
//窗口一卖票,票号为4
//窗口三卖票,票号为3
//窗口三卖票,票号为2
//窗口三卖票,票号为1
方式三:JUC的lock工具
主要学习的是java.util.concurrent.locks.ReentrantLock
使用方式:进入操作共享数据逻辑之前,使用lock.lock()为线程加锁(假设实例化的ReentrantLcok对象名为lock),操作共享数据结束后释放锁:lock.unlock(),具体例子如下(这里使用了匿名内部类简化线程的创建):
public class ThreadDemo1 {
public static void main(String[] args) {
Ticket ticket = new Ticket();
//普通写法
new Thread(new Runnable() {
@Override
public void run() {
for (int i = 0; i < 10; i++){
ticket.sale_ticket();
}
}
}, "一").start();
//lambda表达式写法
new Thread(() -> {
for (int i = 0; i < 10; i++) {
ticket.sale_ticket();
}
}, "二").start();
new Thread(()->{
for (int i = 0; i < 10; i++) {
ticket.sale_ticket();
}
}, "三").start();
}
}
class Ticket{
private int tickets_number = 10;
Lock lock = new ReentrantLock();
void sale_ticket(){
lock.lock();
try {
if(tickets_number > 0){
System.out.println("线程" + Thread.currentThread().getName() + "卖第"+tickets_number--+"张票,还剩" + tickets_number + "张票" );
}
}catch (Exception e){
e.printStackTrace();
}finally{
lock.unlock();
}
}
}
//运行结果
//线程一卖第10张票,还剩9张票
//线程一卖第9张票,还剩8张票
//线程一卖第8张票,还剩7张票
//线程一卖第7张票,还剩6张票
//线程二卖第6张票,还剩5张票
//线程一卖第5张票,还剩4张票
//线程三卖第4张票,还剩3张票
//线程一卖第3张票,还剩2张票
//线程三卖第2张票,还剩1张票
//线程一卖第1张票,还剩0张票