Day14-多线程
多线程
线程简介
进程和线程
进程是执行程序的一次执行过程
- 线程是程序执行的最小单位,而进程是操作系统分配资源的最小单位;
- 一个进程由一个或多个线程组成,线程是一个进程中代码的不同执行路线
- 进程之间相互独立,但同一进程下的各个线程之间共享程序的内存空间(包括代码段,数据集,堆等)及一些进程级的资源(如打开文件和信号等),某进程内的线程在其他进程不可见;
注意:
真正的多线程是多cpu,如果只有一个cpu,同一个时间点内只能执行一个代码,切换得快,就有同步执行的错觉
线程实现⭐⭐⭐
线程创建
Runnable接口(重点)
1.自定义线程类实现Runnable接口
2.重写run()方法,编写线程执行体
3.创建线程对象,调用start()方法启动线程
package Thread;
public class RunnableT implements Runnable { //实现runnable接口
@Override
public void run() { //重写run方法
for (int i = 0; i < 200; i++) {
System.out.println("测试接口");
}
}
public static void main(String[] args) {
RunnableT runnableT = new RunnableT(); //创建对象
new Thread(runnableT).start(); //调用start方法
for (int i = 0; i <2000 ; i++) {
System.out.println("主线程代码");
}
}
}
Thread类(实现了Runnable接口)
- 自定义线程类继承Thread类
- 重写run()方法,编写线程执行体
- 创建线程对象,调用start()方法启动线程
package Thread;
public class ThreadClass extends Thread { //继承线程类
@Override
public void run() { //重写run方法
for (int i = 0; i < 20; i++) {
System.out.println("测试多线程");
}
}
public static void main(String[] args) {
ThreadClass threadClass = new ThreadClass();
threadClass.start(); //调用strat方法
//正常调用run方法会在run方法执行后,在执行主线程main中的代码,但是start让其变成了交替执行,模拟多线程
for (int i = 0; i < 2000; i++) {
System.out.println("区别");
}
}
}
重点
1.不推荐使用继承Thread类的方式实现多线程,避免OOP单继承局限性
2.使用Runnable接口,较为灵活,一个对象可被多个线程使用
例子
同时遍历多个文件夹
package Thread;
import java.io.File;
public class ThreadTest extends Thread {
private String url;
public ThreadTest(String url){
this.url = url;
}
@Override
public void run() {
ImgRead imgRead = new ImgRead();
imgRead.listFile(new File(url));
}
public static void main(String[] args) {
ThreadTest t1 = new ThreadTest("D:\\code\\JavaSE\\基础语法");
ThreadTest t2 = new ThreadTest("D:\\code\\vue");
t1.start();
t2.start();
}
}
class ImgRead{
public static void listFile(File file){
File[] files = file.listFiles(); //用于存放变量
System.out.println(file.getAbsoluteFile()); //输出传递参数的绝对地址
if (files!=null && files.length>0){ //判断是否继续循环,递归出口
for(File file1:files){ //遍历文件夹中的文件
if(file1.isDirectory()){ //递归出口
listFile(file1);
}
}
}
}
}
模拟抢票
package Thread;
//出现问题,多个线程使用同一个资源,数据紊乱
public class RunnableTest implements Runnable {
private int ticket=10;
@Override
public void run() {
while (true){
if(ticket<=0){
break;
}
try {
Thread.sleep(1000); //延时,以免cpu速度太快,全部被一个线程抢光
} catch (InterruptedException e) {
e.printStackTrace();
}
System.out.println(Thread.currentThread().getName()+"拿到了第"+ticket--+"张票");
}
}
public static void main(String[] args) {
RunnableTest runnableTest = new RunnableTest();
new Thread(runnableTest,"学生").start();
new Thread(runnableTest,"老师").start();
new Thread(runnableTest,"黄牛").start();
}
}
龟兔赛跑
package Thread;
public class Race implements Runnable {
public static String winner;
@Override
public void run() {
for (int i = 1; i <= 2000; i++) {
//让兔子睡觉
if(Thread.currentThread().getName().equals("兔子")){
try {
Thread.sleep(100);
} catch (InterruptedException e) {
e.printStackTrace();
}
}
//判断胜利者,结束循环
boolean flage =gameOver(i);
if(flage){
break;
}
System.out.println(Thread.currentThread().getName()+"跑了"+i+"步");
}
}
//判断是否完成比赛
public Boolean gameOver(int steps){
//判断是否有胜利者
if(winner!=null){
return true;
}{
if(steps==2000){
winner=Thread.currentThread().getName();
System.out.println("Winner is "+winner);
return true;
}
}
return false;
}
public static void main(String[] args) {
Race race = new Race();
new Thread(race,"兔子").start();
new Thread(race,"乌龟").start();
}
}
Callable(了解)
1.实现Callable接口,需要返回值类型
2.重写call方法,需要抛出异常
3.创建目标对象
4.创建执行服务 ExecutorService xx = Executors.newFixedThreadPool(number);
5.提交执行 Future
6.获取结果 T xxxx = xxx.get();
7.关闭服务 xx.shutdownNow
package Thread;
import java.io.File;
import java.util.concurrent.*;
/**
* 1.实现Callable接口
* 2.重写call方法
* 3.创建对象
* 4.开启服务
* 5.提交执行
* 6.获取结果
* 7.关闭服务
*/
public class CallableT implements Callable<Boolean> {
private String url;
public CallableT(String url){
this.url = url;
}
@Override
public Boolean call() throws Exception {
ImgRead imgRead = new ImgRead();
imgRead.listFile(new File(url));
return true;
}
public static void main(String[] args) throws ExecutionException, InterruptedException {
CallableT t1 = new CallableT("D:\\code\\JavaSE\\基础语法");
CallableT t2 = new CallableT("D:\\code\\vue");
//创建执行服务
ExecutorService service = Executors.newFixedThreadPool(2);
//提交执行
Future<Boolean> r1 = service.submit(t1);
Future<Boolean> r2 = service.submit(t2);
//获取结果
boolean re1 = r1.get();
boolean re2 = r2.get();
//关闭服务
service.shutdownNow();
}
}
class ImgRead{
public static void listFile(File file){
File[] files = file.listFiles(); //用于存放变量
System.out.println(file.getAbsoluteFile()); //输出传递参数的绝对地址
if (files!=null && files.length>0){ //判断是否继续循环,递归出口
for(File file1:files){ //遍历文件夹中的文件
if(file1.isDirectory()){ //递归出口
listFile(file1);
}
}
}
}
}
特点
1.可以抛出异常
静态代理模式
1.真实对象和代理对象实现同一个接口
2.代理对象要代理真实对象
优点:
1.代理对象可以实现很多真实对象做不了的事情
2.真实对象可以专注于自身
import java.util.Scanner;
public class StaticProxy {
public static void main(String[] args) {
Scanner sc = new Scanner(System.in);
String str =sc.nextLine();
Company company = new Company(new You(str));
company.marry();
sc.close();
}
}
interface Marry{
void marry();
}
class You implements Marry{
private String name;
public You(String name) {
this.name = name;
}
@Override
public void marry() {
System.out.println(name+"要结婚了");
}
}
class Company implements Marry{
Marry x ;
public Company(Marry x) {
this.x = x;
}
@Override
public void marry() {
before();
this.x.marry();
after();
}
private void after() {
System.out.println("婚庆公司收尾款");
}
private void before() {
System.out.println("婚庆公司布置现场");
}
}
Lamda表达式
函数式编程,避免匿名内部类定义过多,简化代码
函数式接口
只包含一个抽象方法,如Runnable接口,其中只包含一个run方法
public class LambdaT {
public static void main(String[] args) {
Test test = new DoTest(); //接口无法直接实例化,所以需要实例化实现类
test.test(1);
test=a -> System.out.println("测试Lambda次数:"+a);
test.test(2); //当参数只有一个时,可以直接省略括号
//当方法体只有一句时,可以省略花括号
Test2 test2 = (a,name)->{ //多个参数类型要么全去掉,要么全加上
System.out.println(a);
System.out.println(name);
};
test2.test(15,"程晨橙");
}
}
interface Test{
void test(int a);
}
class DoTest implements Test{
@Override
public void test(int a) {
System.out.println("测试Lambda次数:"+a);
}
}
interface Test2{
void test(int a,String name);
}
重点
1.接口必须是函数式接口
2.只有一行语句才能简化成一行,否则使用代码块包裹
3.多个参数时,数据类型要么全去掉,要么全加上
线程状态
package Thread;
public class ThreadState {
public static void main(String[] args) throws InterruptedException {
Thread thread = new Thread(()->{
for (int i = 0; i <5 ; i++) {
try {
Thread.sleep(1000);
} catch (InterruptedException e) {
e.printStackTrace();
}
}
System.out.println("线程结束");
}) ;
Thread.State state = thread.getState();
System.out.println(state);
//启动线程
thread.start();
state=thread.getState();
System.out.println(state);
//线程等待
for (int i = 0; i <10 ; i++) {
Thread.sleep(100);
state=thread.getState();
System.out.println(state);
}
System.out.println("//////");
state=thread.getState();
System.out.println(state);
}
}
线程停止
1.通过次数判断来停止线程
2.通过标志位来停止线程
3.最好不要使用stop等自带的方法来停止线程,已经过时,我们可以自己写一个
public class ThreadStop implements Runnable {
private boolean flag = true;
@Override
public void run() {
int i = 0;
while (flag){ //此处如果写成flag=true,程序将会陷入死循环,因为相当于我们重新给flag赋值了true,无法通过stop方法改变标志位的值
System.out.println("线程正在运行-------"+i++);
}
}
public void stop(){
this.flag=false;
}
public static void main(String[] args) {
ThreadStop threadStop = new ThreadStop();
new Thread(threadStop).start();
for (int i = 0; i <1000 ; i++) {
System.out.println("主线程执行次数"+i);
if(i==800){
threadStop.stop();
System.out.println("线程停止");
}
}
}
}
线程休眠
1.sleep指定当前线程的阻塞毫秒数
2.sleep存在异常interruptedException,需要抛出
3.sleep时间达到后,线程进入就绪状态,等待cpu调度
4.sleep可以模拟网络延时,倒计时
5.sleep不会释放锁
模拟网络延时
package Thread;
//模拟网络延时:放大问题发生性
public class SleepTest implements Runnable {
private int ticket=10;
@Override
public void run() {
while (true){
if(ticket<=0){
break;
}
try {
Thread.sleep(1000); //延时,以免cpu速度太快,全部被一个线程抢光
} catch (InterruptedException e) {
e.printStackTrace();
}
System.out.println(Thread.currentThread().getName()+"拿到了第"+ticket--+"张票");
}
}
public static void main(String[] args) {
SleepTest sleepTest = new SleepTest();
new Thread(sleepTest,"学生").start();
new Thread(sleepTest,"老师").start();
new Thread(sleepTest,"黄牛").start();
}
}
倒计时
package Thread;
public class ThreadSleep {
public static void main(String[] args) throws InterruptedException {
ten();
}
public static void ten() throws InterruptedException {
for (int i = 10; i >0 ; i--) {
Thread.sleep(1000);
System.out.println(i);
}
}
}
线程礼让
1.让当前正在执行的线程暂停,但不阻塞
2.将线程从运行状态转换位就绪状态
3.让cpu重新调度,礼让不一定成功
package Thread;
public class ThreadYieldTest {
public static void main(String[] args) {
MyYield myYield = new MyYield();
new Thread(myYield,"a").start(); //正常多线程也会出现结果交替现象,这里可以增大几率
new Thread(myYield,"e").start();
}
}
class MyYield implements Runnable{
@Override
public void run() {
System.out.println(Thread.currentThread().getName()+"线程开始执行");
Thread.yield();
System.out.println(Thread.currentThread().getName()+"线程执行完毕");
}
}
线程强制执行
合并线程,这个线程执行,其他线程阻塞
package Thread;
public class ThreadJoin implements Runnable {
@Override
public void run() {
for (int i = 0; i <1000 ; i++) {
System.out.println("插队了"+i);
}
}
public static void main(String[] args) throws InterruptedException {
ThreadJoin threadJoin = new ThreadJoin();
Thread thread = new Thread(threadJoin);
thread.start();
for (int i = 0; i <1000 ; i++) {
System.out.println("主线程"+i);
if(i==200){
thread.join();
}
}
}
}
线程优先级
使用setPriority(number)设置优先级1-10,数字越大,有越高的概率先执行,先设置再启动线程,默认为5,公平竞争
守护线程
1.线程分为用户线程和守护线程
2.虚拟机必须确保用户线程执行完毕,如main
3.虚拟机不用等待守护线程执行完毕
4.守护线程,如垃圾回收,后台操作日志
package Thread;
public class ThreadDaemonTest {
public static void main(String[] args) {
God god = new God();
Me me = new Me();
Thread thread = new Thread(god);
thread.setDaemon(true); //默认为false,用户线程
thread.start();
new Thread(me).start();
}
}
class God implements Runnable{
@Override
public void run() {
while (true){
System.out.println("上帝永生不死");
}
}
}
class Me implements Runnable{
@Override
public void run() {
for (int i = 0; i < 36500; i++) {
System.out.println("人生不过三万六千五百天");
}
System.out.println("GoodBye,World!");
}
}
线程同步⭐⭐⭐
为保证数据在方法中被访问时的正确性,加入锁机制,当一个线程获得对象的排它锁,独占资源,其他线程必须等待
并发
同一个对象被多个线程同时操作
线程不安全例子
package Thread;
import java.util.ArrayList;
import java.util.List;
public class ThreadUnsafe {
public static void main(String[] args) throws InterruptedException {
//取票
BuyTicket buyTicket = new BuyTicket();
new Thread(buyTicket,"老师").start();
new Thread(buyTicket,"学生").start();
new Thread(buyTicket,"黄牛").start();
System.out.println();
System.out.println();
Thread.sleep(1000);
//取钱
Account account = new Account("创业基金",10_0000);
Blank me = new Blank(account,50000,"我");
Blank badPeople = new Blank(account,10_0000,"law");
me.start();
badPeople.start();
System.out.println();
System.out.println();
Thread.sleep(1000);
System.out.println();
System.out.println();
//不安全的集合
List<String> list = new ArrayList<>();
for (int i = 0; i < 10000; i++) {
new Thread(()->{
list.add(Thread.currentThread().getName());
}).start();
}
System.out.println(list.size());
}
}
//买票
class BuyTicket implements Runnable{
private int ticket = 10;
private boolean flag = true;
@Override
public void run() {
while (flag){
try {
buy();
} catch (InterruptedException e) {
e.printStackTrace();
}
}
System.out.println("票卖完了");
}
public void buy() throws InterruptedException {
if(ticket<=0){
flag=false;
return;
}
Thread.sleep(100);
System.out.println(Thread.currentThread().getName()+"拿到了第"+ticket--+"张票");
}
}
//取钱
class Account{ //账户
String name;
int money;
public Account(String name, int money) {
this.name = name;
this.money = money;
}
}
class Blank extends Thread{
Account account; //账户
int drawMoney; //取了多少钱
int nowMoney; //手上有多少钱
public Blank(Account account, int drawMoney, String name) {
super(name); //继承了thread类,此时代表线程名字
this.account = account;
this.drawMoney = drawMoney;
}
@Override
public void run() {
if(account.money-drawMoney<0){
System.out.println(Thread.currentThread().getName()+"钱不够,取不了");
return;
}
try {
Thread.sleep(1000); //进行等待,放大问题,否则观察不到
} catch (InterruptedException e) {
e.printStackTrace();
}
account.money=account.money-drawMoney;
nowMoney=nowMoney+drawMoney;
System.out.println(account.name+"账户余额"+account.money);
//Thread.currentThread().getName() = this.getName() 因为继承了Thread类
System.out.println(this.getName()+"手上的钱"+nowMoney);
}
}
同步方法
使用synchronized关键字,给做出修改的对象上一把锁,默认为当前对象
一般分为两种,针对方法,加一个synchronized修饰符,但当上锁内容较多时,影响效率
针对部分代码,使用代码块的方式
synchronized(对象){
代码块
}
将例子进行修改
package Thread;
import java.util.ArrayList;
import java.util.List;
public class ThreadUnsafe {
public static void main(String[] args) throws InterruptedException {
//取票
BuyTicket buyTicket = new BuyTicket();
new Thread(buyTicket,"老师").start();
new Thread(buyTicket,"学生").start();
new Thread(buyTicket,"黄牛").start();
System.out.println();
System.out.println();
Thread.sleep(1000);
//取钱
Account account = new Account("创业基金",10_0000);
Blank me = new Blank(account,50000,"我");
Blank badPeople = new Blank(account,10_0000,"law");
me.start();
badPeople.start();
System.out.println();
System.out.println();
Thread.sleep(1000);
System.out.println();
System.out.println();
//不安全的集合
List<String> list = new ArrayList<>();
for (int i = 0; i < 10000; i++) {
synchronized (list){
new Thread(()->{
list.add(Thread.currentThread().getName());
}).start();
}
}
Thread.sleep(1000);
System.out.println(list.size());
}
}
//买票
class BuyTicket implements Runnable{
private int ticket = 10;
private boolean flag = true;
@Override
public void run() {
while (flag){
try {
Thread.sleep(100); //休眠,每次拿完票,都应该停一会,让下一个线程拿票
buy();
} catch (InterruptedException e) {
e.printStackTrace();
}
}
System.out.println("票卖完了");
}
public synchronized void buy() throws InterruptedException {
if(ticket<=0){
flag=false;
return;
}
System.out.println(Thread.currentThread().getName()+"拿到了第"+ticket--+"张票");
}
}
//取钱
class Account{ //账户
String name;
int money;
public Account(String name, int money) {
this.name = name;
this.money = money;
}
}
class Blank extends Thread{
Account account; //账户
int drawMoney; //取了多少钱
int nowMoney; //手上有多少钱
public Blank(Account account, int drawMoney, String name) {
super(name); //继承了thread类,此时代表线程名字
this.account = account;
this.drawMoney = drawMoney;
}
@Override
public void run() {
synchronized (account){
if(account.money-drawMoney<0){
System.out.println(Thread.currentThread().getName()+"钱不够,取不了");
return;
}
try {
Thread.sleep(1000); //进行等待,放大问题,否则观察不到
} catch (InterruptedException e) {
e.printStackTrace();
}
account.money=account.money-drawMoney;
nowMoney=nowMoney+drawMoney;
System.out.println(account.name+"账户余额"+account.money);
//Thread.currentThread().getName() = this.getName() 因为继承了Thread类
System.out.println(this.getName()+"手上的钱"+nowMoney);
}
}
}
注意:应当在适当的地方让线程进行休眠,否则当cpu速度较快时,会出现诸如票被同个线程抢光的情况
Lock
lock接口是控制多个线程对共享资源进行访问的工具
死锁
都需要对方的资源才能继续运行,导致两个线程都无法执行,死锁
某个同步块同时拥有两个以上的锁,就可能发生死锁情况
public class DeadLock {
public static void main(String[] args) {
People people1 = new People("成成陈");
People people2 = new People("陈琛琛");
people1.start();
people2.start();
}
}
class Mouse{
}
class KeyBoard{
}
class People extends Thread{
private String name;
public People(String name){
this.name=name;
}
@Override
public void run() {
play();
}
//使用static保证只有一份资源
static Mouse mouse = new Mouse();
static KeyBoard keyBoard = new KeyBoard();
private void play(){
if(name=="陈琛琛"){
synchronized (mouse){
System.out.println(this.name+"拿到了鼠标");
synchronized (keyBoard){
System.out.println(this.name+"拿到了键盘");
}
}
}else{
synchronized (keyBoard){
System.out.println(this.name+"拿到了键盘");
synchronized (mouse){
System.out.println(this.name+"拿到了鼠标");
}
}
}
}
}
ReentrantLock 可重入锁
该类实现了Lock,拥有和synchronized相同的并发性和内存语义,在实现线程安全的控制中比较常用
import java.util.concurrent.locks.ReentrantLock;
public class ReentrantLockTest {
public static void main(String[] args) {
BuyTickeds buyTickeds = new BuyTickeds();
new Thread(buyTickeds,"黄牛").start(); //此时会出现一个线程将票全抢完的情况
new Thread(buyTickeds,"老师").start();
new Thread(buyTickeds,"学生").start();
}
}
class BuyTickeds implements Runnable{
int ticked = 1000;
private final ReentrantLock lock = new ReentrantLock();
@Override
public void run() {
try {
lock.lock(); //加锁
while (true){
if(ticked>0){
System.out.println(Thread.currentThread().getName()+"拿到了第"+ticked--+"张票");
}else{break;}
}
}finally {
lock.unlock();
}
}
}
线程协作
生产者和消费者问题
1.仓库中只能存放一个产品,生成者将产品放入仓库,消费者从仓库中取走产品
2.仓库中没有物品,生产者将产品放入仓库,如果仓库中有产品,生产者停止生产,直到仓库中的产品被取走
3.仓库中有产品,消费者取走产品,仓库中没有产品,等待,直到仓库中有产品
线程通信
生产者和消费者都共享一个资源,且相互影响
原理
1.生产者没有生产出产品时,需要通知消费者等待
2.消费者在取走产品时,需要告诉生产者开始生产
问题
synchronized只能实现同步,不不能实现线程间的消息传递
工具
wait()
表示线程一直等待,直到被其他线程通知,释放锁
notify
唤醒一个处于等待状态的线程
解决方式一:管程法
增加缓冲区,缓冲区存放生产者生产的产品
public class PipeMethod {
public static void main(String[] args) {
BufferChicken bufferChicken = new BufferChicken();
new Producer(bufferChicken).start();
new Customer(bufferChicken).start();
}
}
class Producer extends Thread{
BufferChicken container;
public Producer(BufferChicken container){
this.container=container;
}
@Override
public void run() {
for (int i = 0; i < 100; i++) {
container.push(new Chicken(i));
System.out.println("生产了第"+i+"只鸡");
}
}
}
class Customer extends Thread{
BufferChicken container;
public Customer(BufferChicken container){
this.container=container;
}
@Override
public void run() {
for (int i = 0; i < 100; i++) {
System.out.println("吃了第"+container.pop().id+"只鸡"); //pop方法返回了一个chicken对象,再调用该对象的id,查看是哪一只鸡
}
}
}
class Chicken{
int id;
public Chicken(int id){
this.id=id;
}
}
class BufferChicken{
//需要一个容器
Chicken[] chickens = new Chicken[10];
//容器计数器
int count = 0;
//生产者放入产品
public synchronized void push(Chicken chicken) {
while (count==chickens.length){
//产品已满,通知消费者消费
System.out.println("已满");
try {
this.wait();
}catch (InterruptedException e){
e.printStackTrace();
}
}
//如果没满则需要生产
chickens[count] = chicken;
count++;
//通知生产者消费
this.notifyAll();
}
//消费者消费产品
public synchronized Chicken pop(){
while (count==0){
//没有产品,等待生产者生产
System.out.println("没得吃");
try {
this.wait();
}catch (InterruptedException e){
e.printStackTrace();
}
}
//如果可以消费
count--;
Chicken chicken =chickens[count];
this.notifyAll();
return chicken; //返回吃了哪只鸡
}
}
解决方式二:信号灯法
增加标志位
public class SignalLampMethod {
public static void main(String[] args) {
TV tv = new TV();
new Actor(tv).start();
new Watcher(tv).start();
}
}
//生产者--->演员
class Actor extends Thread{
TV tv;
public Actor(TV tv){
this.tv=tv;
}
@Override
public void run() {
for (int i = 0; i <10 ; i++) {
if(i%2==0){
tv.perform("《舞蹈风暴》");
}else{
System.out.println("广告之后,精彩继续");
}
}
}
}
//消费者-->观众
class Watcher extends Thread{
TV tv;
public Watcher(TV tv){
this.tv=tv;
}
@Override
public void run() {
for (int i = 0; i <20 ; i++) {
tv.watch();
}
}
}
//产品-->电视节目
class TV{
//标志位
boolean flag = true;
public synchronized void perform(String voices){
if (flag){
//当节目正在播放时,演员进行等待
try {
this.wait();
} catch (InterruptedException e) {
e.printStackTrace();
}
}
System.out.println("现在正在播放的是"+voices);
this.notifyAll();
this.flag=!flag;
}
public synchronized void watch(){
if(!flag){
//节目没有开始,进行等待
try {
this.wait();
} catch (InterruptedException e) {
e.printStackTrace();
}
}
//观看完节目,让演员继续表演
this.notifyAll();
this.flag=!flag;
}
}
线程池
将创建好的多个线程放入线程池中,使用时直接获取,使用完放回池子中,避免频繁的创建和销毁线程
好处
1.提高响应速度
2.降低资源消耗
3.便于线程管理
步骤(Runnable)
1.创建线程池
ExecutorService xx = Executors.newFixedThreadPool(number);
2.执行服务
xx.execute(new xxx());
3.关闭服务
xx.shutdownNow();

浙公网安备 33010602011771号