这是刘意老师的JAVA基础教程的笔记
讲的贼好,附上传送门
一、线程的引入
1.多线程概述
- 进程
a.正在运行的程序,是系统进行资源分类和调用的独立单位。
b.每个进程都有它自己的内存空间和系统资源。 - 线程
a.是进程中的单个顺序控制流,是一条执行路径。
b.一个进程如果只有一条路径,则称为单线程程序。
c.一个进程如果有多条执行路径,则称为多线程程序。 - 小结
线程多的进程抢到CPU执行权的概率大,但是仍具有随机性。
2.Java程序运行原理
Java命令会启动Java虚拟机,启动JVM,等于启动了一个应用程序,也就是启动了一个进程。该进程会自动启动一个“主线程”,然后主线程去调用某个类的main方法。
3.并发和并行
- 并发
物理上的同时发生,并发是在同一个时间点上同时发生。 - 并行
逻辑上的同时发生,并行是在同一个时间段上同时发生。
二、线程的使用(方式1)
1.多线程的使用举例
创建新执行线程有两种方法。一种方法是将类声明为 Thread 的子类。该子类应重写 Thread 类的 run 方法。接下来可以分配并启动该子类的实例。start方法是先启动线程,然后由JVM调用线程的run方法。
public class Demo {
public static void main(String[] args) {
// TODO Auto-generated method stub
MyThread my1=new MyThread();
MyThread my2=new MyThread();
my1.start();//启动一个进程
my2.start();
}
}
class MyThread extends Thread{
@Override
public void run() {
// TODO Auto-generated method stub
for(int i=0;i<1000;i++){
System.out.println(i);
}
}
}
2.线程的基本使用
- 获取名称
public class Demo {
public static void main(String[] args) {
// TODO Auto-generated method stub
MyThread my1 = new MyThread();
MyThread my2 = new MyThread();
my1.start();
my2.start();
}
}
class MyThread extends Thread {
@Override
public void run() {
// TODO Auto-generated method stub
for (int i = 0; i < 1000; i++) {
System.out.println(getName() + ":" + i);
}
}
}
public class Demo {
public static void main(String[] args) {
// TODO Auto-generated method stub
Thread main_thread = Thread.currentThread();
System.out.println(main_thread.getName());
}
}
- 设置线程名称
可以通过构造函数,也可以通过setName方法
public class Demo {
public static void main(String[] args) {
// TODO Auto-generated method stub
// MyThread my1 = new MyThread();
// MyThread my2 = new MyThread();
// my1.setName("my1");
// my2.setName("my2");
MyThread my1 = new MyThread("my1");
MyThread my2 = new MyThread("my2");
my1.start();
my2.start();
}
}
class MyThread extends Thread {
@Override
public void run() {
// TODO Auto-generated method stub
for (int i = 0; i < 1000; i++) {
System.out.println(getName() + ":" + i);
}
}
public MyThread(){
super();
}
public MyThread(String name){
super(name);
}
}
- 线程调度
设置优先级,但是线程优先级也只是标识线程获得CPU控制权的几率高。
public class Demo {
public static void main(String[] args) {
// TODO Auto-generated method stub
MyThread my1 = new MyThread("my1");
MyThread my2 = new MyThread("my2");
System.out.println(my1.getName()+"---"+my1.getPriority());
System.out.println(my2.getName()+"---"+my2.getPriority());
my1.setPriority(1);
my2.setPriority(10);
my1.start();
my2.start();
}
}
class MyThread extends Thread {
@Override
public void run() {
// TODO Auto-generated method stub
for (int i = 0; i < 1000; i++) {
System.out.println(getName() + ":" + i);
}
}
public MyThread(String name){
super(name);
}
- 线程休眠
public class Demo {
public static void main(String[] args) {
// TODO Auto-generated method stub
MyThread my1 = new MyThread("my1");
MyThread my2 = new MyThread("my2");
my1.start();
my2.start();
}
}
class MyThread extends Thread {
@Override
public void run() {
// TODO Auto-generated method stub
for (int i = 0; i < 1000; i++) {
System.out.println(getName() + ":" + i);
try {
Thread.sleep(1000);//这个异常只能try-catch处理,因为父类的run方法没有抛出异常,子类重写方法也不能抛出异常
} catch (InterruptedException e) {
// TODO Auto-generated catch block
e.printStackTrace();
}
}
}
public MyThread(String name){
super(name);
}
}
- 线程加入
指的是调用join方法的线程执行完了,下面的程序才能执行。
public static void main(String[] args) {
// TODO Auto-generated method stub
MyThread my1 = new MyThread("my1");
MyThread my2 = new MyThread("my2");
MyThread my3 = new MyThread("my3");
my1.start();
try {
my1.join();
} catch (InterruptedException e) {
// TODO Auto-generated catch block
e.printStackTrace();
}
my2.start();
try {
my2.join();
} catch (InterruptedException e) {
// TODO Auto-generated catch block
e.printStackTrace();
}
my3.start();
}
join的用法可以参考:java多线程 join方法以及优先级方法,Java多线程中join方法的理解。
(太长不看版= =:join方法就是要等这个自己这个线程走完了,调用启动自己这个线程的主线程才能继续往下走,其他线程不管,具体的可以试试下面的代码)
public class Demo {
public static void main(String[] args) {
MyThread my1 = new MyThread("my1");
MyThread my2 = new MyThread("my2");
MyThread my3 = new MyThread("my3");
my1.start();
my2.start();
try {
my1.join();
} catch (InterruptedException e) {
// TODO Auto-generated catch block
e.printStackTrace();
}
for(int i=0;i<10000;i++){
System.out.println("main");
}
my3.start();
}
}
大概就这个意思
- 线程礼让
只能让线程的执行更加和谐,但不能保证绝对谦让。
public class Demo {
public static void main(String[] args) {
MyThread my1 = new MyThread("my1");
MyThread my2 = new MyThread("my2");
MyThread my3 = new MyThread("my3");
my1.start();
my2.start();
// my3.start();
}
}
class MyThread extends Thread {
@Override
public void run() {
// TODO Auto-generated method stub
for (int i = 0; i < 1000; i++) {
System.out.println(getName() + ":" + i);
Thread.yield();
}
}
public MyThread(String name) {
super(name);
}
}
- 守护线程
将该线程标记为守护线程或用户线程。当正在运行的线程都是守护线程时,Java 虚拟机退出。该方法必须在启动线程前调用。该方法首先调用该线程的 checkAccess 方法,且不带任何参数。这可能抛出 SecurityException(在当前线程中)。
抛出:IllegalThreadStateException - 如果该线程处于活动状态。
SecurityException - 如果当前线程无法修改该线程。
public class Demo {
public static void main(String[] args) {
MyThread my1 = new MyThread("my1");
MyThread my2 = new MyThread("my2");
MyThread my3 = new MyThread("my3");
my1.setDaemon(true);
my2.setDaemon(true);
my3.setDaemon(true);
my1.start();
my2.start();
my3.start();
for(int i=0;i<5;i++){
System.out.println("main");
}
}
}
- 中断线程
stop方法已经不建议使用了。然而Interupt方法我也没太搞懂= =
import java.util.Date;
public class Demo {
public static void main(String[] args) {
MyThread my1 = new MyThread("my1");
my1.start();
try {
Thread.sleep(3000);
my1.interrupt();
} catch (InterruptedException e) {
e.printStackTrace();
System.out.println("线程终止");
}
// my1.stop();
}
}
class MyThread extends Thread {
@Override
public void run() {
System.out.println("开始执行:" + new Date());
try {
Thread.sleep(10000);
} catch (InterruptedException e) {
System.out.println("线程终止");
}
System.out.println("结束执行:" + new Date());
}
public MyThread(String name) {
super(name);
}
}
三、线程的状态
四、线程的使用(第二种方式)
1.多线程实现举例
创建线程的另一种方法是声明实现 Runnable 接口的类。该类然后实现 run 方法。然后可以分配该类的实例,在创建 Thread 时作为一个参数来传递并启动。
public class Demo {
public static void main(String[] args) {
MyRunnable my=new MyRunnable();
Thread t1=new Thread(my);
Thread t2=new Thread(my);
t1.start();
t2.start();
}
}
class MyRunnable implements Runnable {
@Override
public void run() {
// TODO Auto-generated method stub
for (int i = 0; i < 100; i++) {
System.out.println(Thread.currentThread().getName() + ":" + i);
}
}
}
2.那为什么还要有第二种方法呢?
- 避免了java单继承带来的局限性。简单来说就是我已经有一个父类了,那我想实现多线程就不能在继承另一个父类了。
- 适合多个相同程序的代码去处理同一个资源的情况,线程是线程,程序代码是程序代码,数据有效分离开,降低耦合度。体现面向对象的思想。
五、线程的同步性
1.线程同步性概念的引入
还是在模拟卖票的情景中,三个窗口卖一百张票,这三个窗口要共享这一百张票的资源。如果运用如下程序,则两个线程各自从100输出到1,一共输出了200行。
public class Demo1 {
public static void main(String[] args) {
MyThread my1=new MyThread();
MyThread my2=new MyThread();
my1.start();
my2.start();
}
}
class MyThread extends Thread{
static int x=100;//加静态修饰符
@Override
public void run() {
// TODO Auto-generated method stub
for(;x>0;x--){
//标记点1
System.out.println(getName()+":"+x);
//标记点2
}
}
}
然而我们发现,上面的代码结果确实好了一些,但是还是会偶尔出现“卖同一张票”(就是一个数字输出多次),甚至会出现输出负数0。这是为什么呢?原因其实都是:程序的执行具有原子性,并且线程的执行是具有随机性的。当线程1到达标记点1的时候,可能线程2也到达了标记点1,然后当线程1到达标记点2的时候,线程1已经输出了一个x(比如说是100),但是还没来得及进行x--操作,这时候cpu资源假如被线程2抢走,执行了输出语句,这时候由于x--还没有执行,因此输出的还是100,所以即使是static了,也有可能输出两个一样的数字,甚至x--也是两个原子操作,先返回一个x,然后进行x=x-1,可能在线程1执行返回一个i之后,线程2把资源抢跑了,这时候i的值还是没有变化。出现负数的愿意也是同理,假如现在两个线程都到了标记1并且x现在等于1,然后线程1开始执行,假设已经执行完了x--操作,那么这个时候x就是0了,然后线程2开始走,这时候线程2输出的就0了。
上面的代码即使运用接口的方法创建线程,也有可能出现相同的情况。
考虑多线程的数据安全问题:1.是否是多线程环境。2.是否有共享数据。3.是否有多条数据操作共享数据。
2.同步机制
2.1.synchronized
- 2.1.1.synchronized代码块
synchronized修饰的代码,在一个线程进行的时候,发现这段代码已经上了锁,其他线程不能执行。
public class Demo {
public static void main(String[] args) {
MyRunnable my = new MyRunnable();
Thread t1 = new Thread(my);
Thread t2 = new Thread(my);
t1.start();
t2.start();
}
}
class MyRunnable implements Runnable {
int x = 100;
Object obj = new Object();
@Override
public void run() {
synchronized (obj) {
for (; x > 0; x--) {
System.out.println(Thread.currentThread().getName() + ":" + x);
}
}
}
}
同步代码块的传入对象可以是任意对象。
public class Demo {
public static void main(String[] args) {
MyRunnable my = new MyRunnable();
Thread t1 = new Thread(my);
Thread t2 = new Thread(my);
t1.start();
t2.start();
}
}
class MyRunnable implements Runnable {
int x = 1000;
Object obj1 = new Object();
Object obj2 = new Object();
int a = 0;
@Override
public void run() {
if (a % 2 == 0) {
synchronized (obj1) {
for (; x > 0; x--) {
System.out.println(Thread.currentThread().getName() + ":" + x);
}
}
}else{
synchronized (obj2) {
for (; x > 0; x--) {
System.out.println(Thread.currentThread().getName() + ":" + x);
}
}
}
a++;
}
}
上面这块代码也没出问题,但是我没明白,明明不是同一个锁啊,为什么还没出问题?
- 2.1.2.synchronized方法
同步方法的默认锁对象是this。静态方法的默认锁是当前对象的类对象(.class对象)。
@Override
public synchronized void run() {
for (; x > 0; x--) {
System.out.println(Thread.currentThread().getName() + ":" + x);
}
}
2.2.Lock锁
Lock 实现提供了比使用 synchronized 方法和语句可获得的更广泛的锁定操作。
public class Demo {
public static void main(String[] args) {
MyRunnable st = new MyRunnable();
Thread t1 = new Thread(st);
Thread t2 = new Thread(st);
Thread t3 = new Thread(st);
t1.start();
t2.start();
t3.start();
}
}
class MyRunnable implements Runnable {
int tickets = 100;
Lock lock = new ReentrantLock();
@Override
public void run() {
lock.lock();
try {//解决出现如果异常则锁放不开的问题
for (; tickets > 0;) {
try {
Thread.sleep(100);
} catch (InterruptedException e) {
e.printStackTrace();
}
System.out.println(Thread.currentThread().getName() + ":" + tickets--);
}
} finally {
lock.unlock();
}
}
}
2.3 死锁问题
指两个或者两个以上的线程在执行过程中因争夺资源而产生的一种互相等待的现象。
public class Demo {
public static void main(String[] args) {
Thread t1 = new DeadLock(true);
Thread t2 = new DeadLock(false);
t1.start();
t2.start();
}
}
class MyLock{
public static final Object objA=new Object();
public static final Object objB=new Object();
}
class DeadLock extends Thread{
boolean flag;
public DeadLock(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 objA");
synchronized(MyLock.objA){
System.out.println("else objB");
}
}
}
}
}
2.4.等待唤醒机制
public final void wait(long timeout) throws InterruptedException
在其他线程调用此对象的 notify() 方法或 notifyAll() 方法,或者超过指定的时间量前,导致当前线程等待。
当前线程必须拥有此对象监视器。
此方法导致当前线程(称之为 T)将其自身放置在对象的等待集中,然后放弃此对象上的所有同步要求。出于线程调度目的,在发生以下四种情况之一前,线程 T 被禁用,且处于休眠状态:
其他某个线程调用此对象的 notify 方法,并且线程 T 碰巧被任选为被唤醒的线程。
其他某个线程调用此对象的 notifyAll 方法。
其他某个线程中断线程 T。
大约已经到达指定的实际时间。但是,如果 timeout 为零,则不考虑实际时间,在获得通知前该线程将一直等待。
然后,从对象的等待集中删除线程 T,并重新进行线程调度。然后,该线程以常规方式与其他线程竞争,以获得在该对象上同步的权利;一旦获得对该对象的控制权,该对象上的所有其同步声明都将被恢复到以前的状态,这就是调用 wait 方法时的情况。然后,线程 T 从 wait 方法的调用中返回。所以,从 wait 方法返回时,该对象和线程 T 的同步状态与调用 wait 方法时的情况完全相同。
在没有被通知、中断或超时的情况下,线程还可以唤醒一个所谓的虚假唤醒 (spurious wakeup)。虽然这种情况在实践中很少发生,但是应用程序必须通过以下方式防止其发生,即对应该导致该线程被提醒的条件进行测试,如果不满足该条件,则继续等待。换句话说,等待应总是发生在循环中。
public final void notify()
唤醒在此对象监视器上等待的单个线程。如果所有线程都在此对象上等待,则会选择唤醒其中一个线程。选择是任意性的,并在对实现做出决定时发生。线程通过调用其中一个 wait 方法,在对象的监视器上等待。
例如,唤醒的线程在作为锁定此对象的下一个线程方面没有可靠的特权或劣势。
此方法只应由作为此对象监视器的所有者的线程来调用。通过以下三种方法之一,线程可以成为此对象监视器的所有者:
通过执行此对象的同步实例方法。
通过执行在此对象上进行同步的 synchronized 语句的正文。
对于 Class 类型的对象,可以通过执行该类的同步静态方法。
public class Demo {
public static void main(String[] args) {
Student s = new Student();
SetStudent st = new SetStudent(s);
GetStudent gt = new GetStudent(s);
Thread t1 = new Thread(st);
Thread t2 = new Thread(gt);
t1.start();
t2.start();
}
}
class Student {
int age;
String name;
boolean flag;
}
class SetStudent implements Runnable {
Student s;
int x;
SetStudent(Student s) {
this.s = s;
}
@Override
public void run() {
for (;; x++) {
synchronized (s) {
if (s.flag) {
try {
s.wait();
} catch (InterruptedException e) {
e.printStackTrace();
}
}
if (x % 2 == 0) {
s.name = "zzz";
s.age = 23;
} else {
s.name = "xxx";
s.age = 55;
}
s.flag = true;
s.notify();// 唤醒t2,但是唤醒不一定立即执行,还要进行CPU执行权的争夺。
}
}
}
}
class GetStudent implements Runnable {
Student s;
int x;
GetStudent(Student s) {
this.s = s;
}
@Override
public void run() {
for (;;) {
synchronized (s) {
if (!s.flag) {
try {
s.wait();// t2等待了, 立即释放锁,醒过来的时候从这里醒来。
} catch (InterruptedException e) {
e.printStackTrace();
}
}
System.out.println(s.name + ":" + s.age);
s.flag = false;
s.notify();
}
}
}
}
六、线程组
1.线程组的概述和使用
public class Demo {
public static void main(String[] args) {
method1();
method2();
}
static void method1(){
MyRunnable my=new MyRunnable();
Thread t1=new Thread(my);
Thread t2=new Thread(my);
ThreadGroup tg1=t1.getThreadGroup();
ThreadGroup tg2=t2.getThreadGroup();
System.out.println(tg1.getName()+"---"+tg2.getName());
}
static void method2(){
MyRunnable my=new MyRunnable();
ThreadGroup tg=new ThreadGroup("这是一个新的组");
Thread t1=new Thread(tg,my);
Thread t2=new Thread(tg,my);
System.out.println(t1.getThreadGroup().getName()+"---"+t2.getThreadGroup().getName());
}
}
class MyRunnable implements Runnable{
@Override
public void run() {
for(int x=0;x<100;x++){
System.out.println(Thread.currentThread().getName()+":"+x);
}
}
}
七、线程池
1.概述和使用
程序启动一个新线程成本是比较高的,因为它涉及到要与操作系统进行交互。而使用线程池可以很好的提高性能,尤其是当程序中要创建大量生存期很短的线程时,更应该考虑使用线程池。
线程池里的每一个线程代码结束后,并不会死亡,而是再次回到线程池中成为空闲状态,等待下一个对象来使用。
在JDK5之前,我们必须手动实现自己的线程池,从JDK5开始,Java内置支持线程池。
public class Demo {
public static void main(String[] args) {
ExecutorService pool=Executors.newFixedThreadPool(2);
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()+":"+x);
}
}
}
2.线程的实现(第三种方法)
public interface Callable<V>
返回结果并且可能抛出异常的任务。实现者定义了一个不带任何参数的叫做 call 的方法。Callable 接口类似于 Runnable,两者都是为那些其实例可能被另一个线程执行的类设计的。但是 Runnable 不会返回结果,并且无法抛出经过检查的异常。
public interface Future<V>
Future 表示异步计算的结果。它提供了检查计算是否完成的方法,以等待计算的完成,并获取计算的结果。计算完成后只能使用 get 方法来获取结果,如有必要,计算完成前可以阻塞此方法。取消则由 cancel 方法来执行。还提供了其他方法,以确定任务是正常完成还是被取消了。一旦计算完成,就不能再取消计算。如果为了可取消性而使用 Future 但又不提供可用的结果,则可以声明 Future<?> 形式类型、并返回 null 作为底层任务的结果。
public class Demo {
public static void main(String[] args) throws InterruptedException, ExecutionException {
ExecutorService pool=Executors.newFixedThreadPool(2);
Future<Integer> f1=pool.submit(new MyCallable(100));
Future<Integer> f2=pool.submit(new MyCallable(200));
Integer i1=f1.get();
Integer i2=f2.get();
System.out.println(i1+":"+i2);
pool.shutdown();
}
}
class MyCallable implements Callable<Integer>{
int number;
public MyCallable(int number) {
this.number=number;
}
@Override
public Integer call() throws Exception {
int sum=0;
for(int x=0;x<number;x++){
sum+=x;
}
return sum;
}
}
八、匿名内部类实现多线程
public class Demo {
public static void main(String[] args){
new Thread(){
@Override
public void run() {
for(int x=0;x<100;x++){
System.out.println(Thread.currentThread().getName()+"---"+x);
}
}
}.start();
new Thread(new Runnable(){
@Override
public void run() {
for(int x=0;x<100;x++){
System.out.println(Thread.currentThread().getName()+"---"+x);
}
}
}).start();
new Thread(new Runnable(){
@Override
public void run() {
for(int x=0;x<100;x++){
System.out.println("hello---"+x);
}
}
}){
@Override
public void run() {
for(int x=0;x<100;x++){
System.out.println("world---"+x);
}
}
}.start();
}
}
九、定时器
1.概述和使用
定时器是一个应用十分广泛的线程工具,可用于调度多个定时任务以后台线程的方式执行。在Java中,可以通过Timer和TimerTask类来实现定义调度的功能。
Timer
一种工具,线程用其安排以后在后台线程中执行的任务。可安排任务执行一次,或者定期重复执行。
与每个 Timer 对象相对应的是单个后台线程,用于顺序地执行所有计时器任务。计时器任务应该迅速完成。如果完成某个计时器任务的时间太长,那么它会“独占”计时器的任务执行线程。因此,这就可能延迟后续任务的执行,而这些任务就可能“堆在一起”,并且在上述不友好的任务最终完成时才能够被快速连续地执行。
TimerTask
由 Timer 安排为一次执行或重复执行的任务。
public class Demo{
public static void main(String[] args){
Timer t=new Timer();
t.schedule(new MyTask(), 3000);
t.schedule(new MyTask(t), 6000);
t.schedule(new MyTask(), 9000,200);
}
}
class MyTask extends TimerTask{
Timer t;
public MyTask() {
}
public MyTask(Timer t){
this.t=t;
}
@Override
public void run() {
System.out.println("BANG!");
}
}
**粗体** _斜体_ [链接](http://example.com) `代码` - 列表 > 引用
。你还可以使用@
来通知其他用户。